Creating reporters

Reporters are used to display/save the progress of tests as they are executed. For example, the @japa/spec-reporter shows the progress of tests in the terminal.

In this guide, we will learn how to create and use custom reporters with Japa.

Defining reporters

The reporters are defined as an array within the config object.

import { specReporter } from '@japa/spec-reporter'
import { configure } from '@japa/runner'
configure({
reporters: [
specReporter(),
myCustomReporter()
]
})
const { specReporter } = require('@japa/spec-reporter')
const { configure } = require('@japa/runner')
configure({
reporters: [
specReporter(),
myCustomReporter()
]
})

Creating a reporter

The reporter implementation is a JavaScript function that receives the runner instance as the first argument and the emitter instance as the second argument.

You can listen for specific events to monitor the test's progress and process them as required.

Let's create a custom reporter that logs the test's progress in the terminal.

Step 1. Create and register the reporter

The first step is to define the reporter function and add it to the reporters array.

You can write the reporter implementation within the bin/test.js file or create a new file and import it inside the bin/test.js file.

function logReporter() {
return function (runner, emitter) {
}
}
configure({
reporters: [logReporter()]
})

Step 2. Listening for events

Let's continue with the logReporter implementation and listen for the events to display the test's progress.

Make sure to consult the event emitter documentation to view all the available events and their data.

function logReporter() {
return function (runner, emitter) {
let indentation = 2
function getSpaces() {
return new Array(indentation + 1).join(' ')
}
emitter.on('runner:start', () => {
console.log(`START`)
})
emitter.on('runner:end', () => {
console.log(`FINISH: completed in "${runner.getSummary().duration}ms"`)
})
emitter.on('suite:start', (payload) => {
console.log(`${getSpaces()}SUITE: "${payload.name}"`)
indentation += 2
})
emitter.on('suite:end', (payload) => {
indentation -= 2
console.log(`${getSpaces()}SUITE: "${payload.name}"`)
})
emitter.on('group:start', (payload) => {
console.log(`${getSpaces()}GROUP: "${payload.title}"`)
indentation += 2
})
emitter.on('group:end', (payload) => {
indentation -= 2
console.log(`${getSpaces()}GROUP: "${payload.title}"`)
})
emitter.on('test:end', (payload) => {
console.log(`${getSpaces()}TEST: "${payload.title}" completed in "${payload.duration}ms"`)
})
}
}

Running async operations

If your reporters want to perform async operations like reading/writing to a file or a TCP connection, then make sure to put that logic within the runner:start and runner:end events.

Japa waits for these event listeners to finish before starting or ending the tests suite. Also, it is a good practice to collect all the metrics in memory first and then flush them to disk.

import lockfile from 'proper-lockfile'
let release
emitter.on('runner:start', async () => {
release = await lockfile('somefile.json')
})
emitter.on('runner:end', async () => {
await fs.write('somefile.json', contents)
await release()
})

Base Reporter

To make it easier for you to write custom reporters, we also provide @japa/base-reporter which abstracts the repetitive parts of creating test reporters.

First, you need to install the package from npm registry as follows:

npm i @japa/base-reporter

Then you have create your reporter as follows:

import { BaseReporter } from '@japa/base-reporter'
class MyReporter extends BaseReporter {}
export const reporterFn = (myReporterOptions = {}) => {
const myReporter = new MyReporter(myReporterOptions)
return myReporter.boot.bind(reporter)
}

Handlers

The Base reporter invokes following methods as it receives the events from the runner. You can implement these methods to display the tests progress :

import { BaseReporter } from '@japa/base-reporter'
class MyReporter extends BaseReporter {
protected onTestStart(payload: TestStartNode) {
console.log('test started')
}
protected onTestEnd(payload: TestEndNode) {
console.log('test endeded')
}
protected onGroupStart(payload: GroupStartNode) {
console.log('group started')
}
protected onGroupEnd(payload: GroupEndNode) {
console.log('group ended')
}
protected onSuiteStart(payload: SuiteStartNode) {
console.log('suite started')
}
protected onSuiteEnd(payload: SuiteEndNode) {
console.log('suite ended')
}
protected async start(payload: RunnerStartNode) {
console.log('test runner started. You can run async operations here')
}
protected async end(payload: RunnerEndNode) {
console.log('test runner ended. You can run async operations here')
}
}

Inherited properties

The following properties are available on the BaseReporter. These properties are available only after the boot method is called.

runner

Reference to underlying tests runner instance.

this.runner

currentFileName

Reference to the file name for which tests are getting executed. The filename is only available inside the test or group handlers.

this.currentFileName

currentSuiteName

Reference to the suite name for which tests are getting executed. The suite name is only available after the onSuiteStart handler call.

this.currentSuiteName

uncaughtExceptions

Uncaught exceptions collected while tests are running. We rely on process.on('uncaughtException') event to collect uncaught exceptions and display them with their stack trace at the end.

Printing tests summary

After all the tests have been finished, you can call the printSummary method to print a detailed summary of all tests alongside pretty diffs and pretty error stack trace.

You should call the printSummary method from the end handler.

class SpecReporter extends BaseReporter {
protected async end() {
const summary = await this.runner.getSummary()
await this.printSummary(summary)
}
}