Feb 14, 2020

Supercharging Jest with Custom Reporters

Jest is, no doubt, one of the most popular test runners for the JavaScript ecosystem. It contains just the right amount of features to quickly build testing solutions for all project sizes, without thinking about how the tests should be run, or how snapshots should be managed, as we'd expect from test runners.

One powerful feature that you might not have heard of, is the option to build custom reporters. A Jest reporter is, put simply, a JavaScript class that implements an interface with methods provided by Jest, including onTestResult, onRunStart, onTestStart, onRunComplete, and getLastError. Even the test result screen you usually view upon completion of a test run, is the result of a built-in (default) reporter.

$ jest
 PASS  src/session.spec.ts
 PASS  src/metrics.spec.ts

Test Suites: 2 passed, 2 total
Tests:       5 passed, 5 total
Snapshots:   0 total
Time:        3.786s
Ran all test suites.

You might have thought of sending your test results to an external source, maybe something like Slack? If that's the case, it's your lucky day, because I've built just that.

🃏 Building a custom Jest reporter

I published the complete example project setup including a test suite, the reporter and all packages to my blog code repository, available here.

As the foundation, I chose a simple TypeScript-based setup which uses a yarn script to build all sources including the test suite and my custom reporter to the dist directory before the test suite is run.

The Jest configuration, located in jest.config.js, contains just the minimal configuration to get everything running, the TypeScript configuration outputs code and source maps (which are important for Jest, since I'm not using ts-jest in this case) to the dist directory, as described before.

// jest.config.js

module.exports = {
  // Only search for tests in the dist directory emitted by TypeScript
  roots: ['<rootDir>/dist'],

  // Include all .spec.js files generated by the TypeScript build
  testRegex: '.*.spec.js$',

  // Use default and custom reporter
  reporters: ['default', '<rootDir>/dist/tests/reporter.js']
};
// tsconfig.json

{
  "compilerOptions": {
    "target": "ESNext",
    "module": "commonjs",
    "moduleResolution": "node",

    "rootDir": "./src",
    "outDir": "./dist",

    // Include source maps for Jest
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,

    "strict": true,
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true
  }
}

Now, let's get to the main part of our reporter: I installed two additional packages (@jest/reporters and @jest/test-result) to get hold of the necessary types, which we'll use to create our custom reporter class, implementing the Reporter interface.

// src/tests/reporter.ts

// Those two packages supply the types we need to
// build our custom reporter
import { Reporter, Context } from '@jest/reporters';
import { AggregatedResult } from '@jest/test-result';

// Our reporter implements only the onRunComplete lifecycle
// function, run after all tests have completed
export default class CustomReporter implements Pick<Reporter, 'onRunComplete'> {
  async onRunComplete(_: Set<Context>, results: AggregatedResult) {
    // TODO Add Slack webhook trigger
    console.log('Your report is available!');
  }
}

This is already everything we need the reporter to do, not counting the actual result-handling and message-sending part. We included the transpiled reporter to be used by Jest, let's create a sample test suite, containing a passing test, and a failing one to print pretty error messages.

// src/tests/user.spec.ts

describe('user test suite', () => {
  test('should create user', async () => {
    const mockCreate = () =>
      Promise.resolve({
        id: '02051322-1523-4a25-aa6e-9fb02eb56003',
        name: 'Sample User'
      });

    const createdUser = await mockCreate();

    expect(createdUser.name).toEqual('Sample User');
  });

  test('should update user', async () => {
    const failingMockUpdate = () =>
      Promise.reject(new Error('Could not update user'));

    await failingMockUpdate();
  });
});

If we go ahead and run yarn test, we'll see our logged message from the reporter, as well as the default Jest report in all its glory. The final thing to do now is to set up a Slack webhook and format pretty messages depending on the test results. To get accustomed to the Slack message format, you can visit the Block Kit Builder to get some inspiration and sense of how your report could look like.

🌁 Other use cases

But this does not cover everything you can do with custom reporters! You can get creative, tap into more lifecycle events and use every piece of information your tests expose.

Another great use case could be a reporter that starts up and tears down required dependencies, for example, other services. This is a great alternative to using the global setup and teardown configuration options, since you can maintain state between the two lifecycle phases, as well as reacting to the test outcome. As such, reporters can significantly improve integration testing workflows built completely with native Jest capabilities.


I hope you learned a thing or two, maybe you just got a great idea to take your testing setup to the next level! If you've got any questions, suggestions, or feedback in general, don't hesitate to send a DM or mail 👋