Test-framework agnostic JavaScript testing runner that supports TDD workflows and CI integration across multiple browsers and environments.
—
Testem provides multiple output formats for test results including TAP, XUnit, dot notation, TeamCity integration, and an interactive development UI.
Testem includes several built-in reporters for different output formats and integrations.
const reporters = {
tap: 'TapReporter', // Test Anything Protocol output
xunit: 'XUnitReporter', // XUnit XML format for CI integration
dot: 'DotReporter', // Dot progress indicator
teamcity: 'TeamCityReporter', // TeamCity service messages
dev: 'DevReporter' // Interactive development UI (default for dev mode)
};Usage Examples:
# Command line reporter selection
testem ci -R tap # TAP output
testem ci -R xunit # XUnit XML
testem ci -R dot # Dot notation
testem ci -R teamcity # TeamCity format// Configuration file
{
"reporter": "tap",
"report_file": "test-results.txt"
}Test Anything Protocol (TAP) output format, widely supported by CI systems and test tooling.
/**
* TAP (Test Anything Protocol) reporter
* Outputs TAP-compliant test results
*/
class TapReporter {
constructor(silent?: boolean, out?: NodeJS.WritableStream);
}
// TAP configuration options
interface TapOptions {
tap_failed_tests_only?: boolean; // Only output failing tests
tap_quiet_logs?: boolean; // Only show logs for failed tests
tap_strict_spec_compliance?: boolean; // Strict TAP spec compliance
tap_log_processor?: (log: any) => string; // Custom log processing function
}Example TAP Output:
ok 1 Chrome 91.0 - should add numbers correctly
ok 2 Chrome 91.0 - should handle edge cases
not ok 3 Firefox 89.0 - should validate input
---
message: "Expected 'invalid' to throw error"
severity: fail
data:
at: "test/validation.js:15:5"
...
1..3
# tests 3
# pass 2
# fail 1
# not okUsage Examples:
// Basic TAP configuration
{
"reporter": "tap"
}
// Advanced TAP options
{
"reporter": "tap",
"tap_failed_tests_only": true,
"tap_quiet_logs": true,
"tap_strict_spec_compliance": true
}
// Custom log processor (testem.js only)
module.exports = {
reporter: 'tap',
tap_log_processor: function(log) {
return `[${new Date().toISOString()}] ${log}`;
}
};XML format compatible with JUnit and other CI systems that expect XUnit-style test results.
/**
* XUnit XML reporter for CI integration
* Generates JUnit-compatible XML output
*/
class XUnitReporter {
constructor(silent?: boolean, out?: NodeJS.WritableStream);
}Example XUnit Output:
<?xml version="1.0" encoding="UTF-8" ?>
<testsuite name="Testem Tests" tests="3" failures="1" timestamp="2023-06-15T10:30:45Z" time="5.2">
<testcase classname="Chrome 91.0" name="should add numbers correctly" time="0.1"/>
<testcase classname="Chrome 91.0" name="should handle edge cases" time="0.2"/>
<testcase classname="Firefox 89.0" name="should validate input" time="0.3">
<failure name="should validate input" message="Expected 'invalid' to throw error">
<![CDATA[
AssertionError: Expected 'invalid' to throw error
at test/validation.js:15:5
]]>
</failure>
</testcase>
</testsuite>Usage Examples:
// XUnit with output file
{
"reporter": "xunit",
"report_file": "test-results.xml"
}
// CI integration
{
"reporter": "xunit",
"launch_in_ci": ["Chrome", "Firefox"]
}Minimalist dot-based progress indicator showing test execution status.
/**
* Dot progress reporter
* Shows dots for passing tests, F for failures
*/
class DotReporter {
constructor(silent?: boolean, out?: NodeJS.WritableStream);
}Example Dot Output:
..F...F..
Failures:
1) Chrome 91.0 - should validate input
Expected 'invalid' to throw error
at test/validation.js:15:5
2) Firefox 89.0 - should handle async operations
Timeout after 5000ms
at test/async.js:23:10
9 tests, 2 failuresUsage Examples:
// Dot reporter for quick feedback
{
"reporter": "dot"
}TeamCity service message format for seamless integration with JetBrains TeamCity CI/CD platform.
/**
* TeamCity service messages reporter
* Outputs TeamCity-compatible service messages
*/
class TeamCityReporter {
constructor(silent?: boolean, out?: NodeJS.WritableStream);
}Example TeamCity Output:
##teamcity[testSuiteStarted name='testem.suite']
##teamcity[testStarted name='Chrome 91.0 - should add numbers correctly']
##teamcity[testFinished name='Chrome 91.0 - should add numbers correctly' duration='100']
##teamcity[testStarted name='Firefox 89.0 - should validate input']
##teamcity[testFailed name='Firefox 89.0 - should validate input' message='Expected |'invalid|' to throw error' details='AssertionError: Expected |'invalid|' to throw error|n at test/validation.js:15:5']
##teamcity[testFinished name='Firefox 89.0 - should validate input' duration='300']
##teamcity[testSuiteFinished name='testem.suite' duration='5200']Usage Examples:
// TeamCity integration
{
"reporter": "teamcity"
}Interactive text-based UI for development mode with real-time test feedback and keyboard controls.
/**
* Interactive development reporter
* Provides TUI with test results and browser management
*/
class DevReporter {
constructor(silent?: boolean, out?: NodeJS.WritableStream);
}Features:
Keyboard Controls:
ENTER - Run testsq - Quit←/→ - Navigate browser tabsTAB - Switch panels↑/↓ - ScrollSPACE - Page downb - Page upUsage Examples:
// Dev reporter (default for development mode)
{
"reporter": "dev" // Usually automatic in dev mode
}Create custom reporters by implementing the reporter interface.
/**
* Base reporter interface
*/
interface Reporter {
/**
* Called when test suite starts
* @param numLaunchers - Number of test launchers
*/
onStart(numLaunchers: number): void;
/**
* Called when a test starts
* @param launcher - Launcher information
* @param test - Test information
*/
onTestStart(launcher: LauncherInfo, test: TestInfo): void;
/**
* Called when a test completes
* @param launcher - Launcher information
* @param test - Test result
*/
onTestResult(launcher: LauncherInfo, test: TestResult): void;
/**
* Called when all tests complete
* @param results - Final test results
*/
onAllTestResults(results: TestResults): void;
}
interface LauncherInfo {
name: string; // Launcher name (e.g., "Chrome 91.0")
type: string; // Launcher type ("browser" or "process")
}
interface TestInfo {
name: string; // Test name
id: number; // Test ID
}
interface TestResult extends TestInfo {
passed: boolean; // Test passed
failed: boolean; // Test failed
error?: Error; // Test error
logs: string[]; // Test logs
runDuration: number; // Test duration in ms
}Custom Reporter Example:
// custom-reporter.js
class CustomReporter {
constructor(silent, out) {
this.out = out || process.stdout;
this.silent = silent;
this.results = [];
}
onStart(numLaunchers) {
if (!this.silent) {
this.out.write(`Starting tests on ${numLaunchers} launchers...\n`);
}
}
onTestResult(launcher, test) {
this.results.push({ launcher, test });
if (!this.silent) {
const status = test.passed ? '✓' : '✗';
const duration = `(${test.runDuration}ms)`;
this.out.write(`${status} ${launcher.name} - ${test.name} ${duration}\n`);
}
}
onAllTestResults(results) {
const passed = this.results.filter(r => r.test.passed).length;
const total = this.results.length;
this.out.write(`\nResults: ${passed}/${total} tests passed\n`);
// Output JSON summary
const summary = {
total,
passed,
failed: total - passed,
results: this.results
};
this.out.write(`\n${JSON.stringify(summary, null, 2)}\n`);
}
}
module.exports = CustomReporter;// testem.js
const CustomReporter = require('./custom-reporter');
module.exports = {
src_files: ['src/**/*.js', 'test/**/*.js'],
reporter_options: {
'custom': CustomReporter
}
};Direct reporter output to files for CI integration and archival.
interface ReporterConfig {
reporter: string; // Reporter name
report_file?: string; // Output file path
reporter_options?: object; // Reporter-specific options
}Usage Examples:
// Output to file
{
"reporter": "tap",
"report_file": "results/test-output.tap"
}
// XUnit with timestamped filename
{
"reporter": "xunit",
"report_file": "results/junit-{{timestamp}}.xml"
}
// Multiple output files (testem.js only)
module.exports = {
reporter: 'tap',
after_tests: function(config, data, callback) {
// Custom post-processing
const fs = require('fs');
const results = JSON.stringify(data.results, null, 2);
fs.writeFileSync('results/detailed-results.json', results);
callback();
}
};<!-- Jenkins pipeline -->
<project>
<builders>
<hudson.tasks.Shell>
<command>testem ci -R xunit</command>
</hudson.tasks.Shell>
</builders>
<publishers>
<hudson.tasks.junit.JUnitResultArchiver>
<testResults>test-results.xml</testResults>
</hudson.tasks.junit.JUnitResultArchiver>
</publishers>
</project># .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- run: npm install
- run: testem ci -R tap
- uses: dorny/test-reporter@v1
if: always()
with:
name: Test Results
path: test-results.tap
reporter: java-junitInstall with Tessl CLI
npx tessl i tessl/npm-testem