How to setup a custom test reporter for Jest

As one of the most popular test runners in the Javascript ecosystem, Jest gives us a variety of helpful tools to help us test our code. The test reporter is one of these features.
With a few lines of code, we can effortlessly create visibility into runtime performance and test failures — both of which are super helpful for debugging flakey tests. As an added bonus, we can also make our test reporter more robust by writing in Typescript.
📦 Setting up Jest in our project
To begin, we’ll install some dependencies and setup Typescript.
> yarn init > yarn add typescript jest ts-node @jest/reporters @jest/types -D > yarn tsc --init
After setting up our Typescript project, we need to add a new property called reporters to our Jest configuration. This property accepts an array that we will use to point to our custom reporter.
Note: You can have many reporters, but we’re just creating one.
// jest.config.js
module.exports = {
reporters: ["<rootDir>/test-reporter/index.js"],
};We can then create a new folder called test-reporter and create two files:
index.js— our entry point that we can use to compile Typescripttest-reporter.ts— our custom reporter written in Typescript
> mkdir test-reporter
> cd test-reporter
> touch index.js
> touch test-reporter.tsIn index.js, we can configure our entry point file to compile our reporter module to Typescript at runtime when we run our tests.
// test-reporter/index.js
const tsNode = require("ts-node");
tsNode.register({
transpileOnly: true,
compilerOptions: require("../tsconfig.json").compilerOptions,
});
module.exports = require("./test-reporter");📝 Creating our Custom Reporter
With Typescript set to compile at runtime, we can now write our custom test reporter. Here is a simple test reporter that logs test results when the test runner finishes:
import { Reporter, TestContext } from "@jest/reporters";
import { AggregatedResult } from "@jest/test-result";
type CustomReporter = Pick<Reporter, "onRunComplete">;
export default class TestReporter implements CustomReporter {
constructor() {}
onRunComplete(_: Set<TestContext>, results: AggregatedResult) {
console.log(results);
}
}From results, we receive an aggregate set of test results:
{
numFailedTestSuites: 0,
numFailedTests: 0,
numPassedTestSuites: 0,
numPassedTests: 0,
numPendingTestSuites: 0,
numPendingTests: 0,
numRuntimeErrorTestSuites: 0,
numTodoTests: 0,
numTotalTestSuites: 2,
numTotalTests: 0,
openHandles: [],
snapshot: {
added: 0,
didUpdate: false,
failure: false,
filesAdded: 0,
filesRemoved: 0,
filesRemovedList: [],
filesUnmatched: 0,
filesUpdated: 0,
matched: 0,
total: 0,
unchecked: 0,
uncheckedKeysByFile: [],
unmatched: 0,
updated: 0
},
startTime: 1652692338326,
success: false,
testResults: [],
wasInterrupted: false
}A really useful property here is testResults, which is an array of testResult objects. These contain really useful metadata such as failure messages and open handle detection (useful for flakey tests).
⚙️ Enhancing our Custom Reporter
Thanks to tools given to us by @jest/reporters, we can improve upon our custom reporter in many ways.
First, we can hook into additional lifecycle methods beyond just onRunComplete, such as: onTestResult, onRunStart, onTestStart, onRunComplete, and even getLastError. With Typescript configured, we can ensure these methods have strong type support as well.
Secondly, we can make our reporter more intelligent about when it should run by being able to read our Jest configuration values from the reporter.
import { Reporter, TestContext } from "@jest/reporters";
import { AggregatedResult } from "@jest/test-result";
import { Config } from "@jest/types";
type CustomReporter = Pick<Reporter, "onRunComplete">;
export default class TestReporter implements CustomReporter
{
// Add the config to our constructor
constructor(private config: Config.InitialOptions) {}
onRunComplete(context: Set<TestContext>, results: AggregatedResult) {
const isCi = this.config.ci
// Only run in a CI environment
if (isCi) {
console.log(results);
}
}
}We can also pass in custom options in our Jest configuration.
// jest.config.js
module.exports = {
reporters: [["<rootDir>/test-reporter/index.js", { useReporter: true }]],
};And then access these custom values inside our reporter.
import { Reporter, TestContext } from "@jest/reporters";
import { AggregatedResult } from "@jest/test-result";
import { Config } from "@jest/types";
type CustomReporter = Pick<Reporter, "onRunComplete">;
interface Options {
useReporter: boolean;
}
export default class TestReporter implements CustomReporter {
constructor(
private config: Config.InitialOptions,
private options: Options
) {}
onRunComplete(context: Set<TestContext>, results: AggregatedResult) {
const isCi = this.config.ci;
const useReporter = this.options.useReporter;
if (isCi || useReporter) {
console.log(results);
}
}
}🤔 What should we do with the results?
With an intelligent test reporter at the ready, it would be great to persist our results somewhere. Fortunately, we can do many things here.
- You could persist them as text files and save them as artifacts in your CI environment.
- You could push them to a third-party observability platform, such as Datadog.
- You could write an elaborate bot to ping you on Slack or GitHub.
The choice is yours! 🚀
For more information on Jest reporters, visit the docs or use explore my working code sample here.






