Merging coverage reports from Jest and Cypress

Code coverage is a great metric to know what code your tests aren't touching, so you can have some insight on potential locations where you can focus on writing new tests. It is common to use Jest to run integration tests (with something like react-testing-library) and Cypress to run E2E tests, but both have different configurations to emit coverage reports, and they are separated. How could we merge them into a single one, for easy visualization?

The first time I tried to do this it got very complex where I utilized some libs from instanbul, but I found this repo from @bahmutov and realized it is actually very simple. First, we need to configure both Jest and Cypress to output the coverage in the right places.

Configuring Jest

Jest already has coverage built-in, we only need to configure it. Here is a snippet from package.json:

"jest": {
  "collectCoverage": true,
  "coverageDirectory": "jest-coverage",
  "coverageReporters": ["json"]
},

For what we are doing, we only need the json format when exporting the coverage. I added the collectCoverage flag on the configuration, but you can also pass it as an option on the CLI with --coverage. This way you can still have some code that will run the tests without coverage.

Configuring Cypress

Cypress is a bit harder, it doesn't have coverage build-in so we need to set up it ourselves. First install @cypress/code-coverage, babel-plugin-istanbul, istanbul-lib-coverage and nyc on devDependencies. Explaining each lib:

  • @cypress/code-coverage is the plugin library that will integrate the coverage to Cypress
  • istanbul-lib-coverage and nyc are peer dependencies of @cypress/code-coverage
  • babel-plugin-istanbul is to instrument our code, making it possible to know which line has been executed when running them on Cypress

Now on the cypress/support/index.js file, add the following content (yes, only that):

import '@cypress/code-coverage/support';

And on cypress/plugins/index.js:

module.exports = (on, config) => {
  on('task', require('@cypress/code-coverage/task'));
  on(
    'file:preprocessor',
    require('@cypress/code-coverage/use-browserify-istanbul')
  );
};

Since we are saving the Jest reports in jest-coverage, we need to edit the nyc config to save the Cypress reports to cypress-coverage (yeah, consistency!). Add this to package.json:

"nyc": {
  "report-dir": "cypress-coverage"
}

Now if you run your tests on Cypress, the reports should be in the correct folder.

Merging the reports

Ok, we have both reports on jest-coverage and cypress-coverage folders, now how do we merge them?

There are two nyc commands that can help with this: nyc merge (will merge all reports inside a folder into a file) and nyc report (will get the contents of .nyc_output folder and create the final report). You may have noticed that we need to have the reports in the same folder, and we don't have a .nyc_output yet. You can automate all these steps using only npm scripts but I preferred to write a Node script to do all of that:

/**
 * This script merges the coverage reports from Cypress and Jest into a single one,
 * inside the "coverage" folder
 */
 
const { execSync } = require('child_process');
const fs = require('fs-extra');
 
const REPORTS_FOLDER = 'reports';
const FINAL_OUTPUT_FOLDER = 'coverage';
 
const run = (commands) => {
  commands.forEach((command) => execSync(command, { stdio: 'inherit' }));
};
 
// Create the reports folder and move the reports from cypress and jest inside it
fs.emptyDirSync(REPORTS_FOLDER);
fs.copyFileSync(
  'cypress-coverage/coverage-final.json',
  `${REPORTS_FOLDER}/from-cypress.json`
);
fs.copyFileSync(
  'jest-coverage/coverage-final.json',
  `${REPORTS_FOLDER}/from-jest.json`
);
 
fs.emptyDirSync('.nyc_output');
fs.emptyDirSync(FINAL_OUTPUT_FOLDER);
 
// Run "nyc merge" inside the reports folder, merging the two coverage files into one,
// then generate the final report on the coverage folder
run([
  // "nyc merge" will create a "coverage.json" file on the root, we move it to .nyc_output
  `nyc merge ${REPORTS_FOLDER} && mv coverage.json .nyc_output/out.json`,
  `nyc report --reporter lcov --report-dir ${FINAL_OUTPUT_FOLDER}`,
]);

This script will:

  1. Create or clear the reports folder
  2. Copy the files from cypress-coverage and jest-coverage into the reports folder
  3. Create or clear the .nyc_output and coverage folders
  4. Run the nyc merge command, that will generate a coverage.json file at the root of the project, and move it inside .nyc_output
  5. Run the nyc report command, that will get the .nyc_output/out.json file and generate the full report inside the coverage folder

And that's it! You may also want to delete all the created temp folders (besides coverage) at the end of the process. I just added them to my .gitignore. Now you can run the script after you finished running all the tests, like this:

"scripts": {
  "test-coverage": "yarn test:jest && yarn test:cypress && node ./scripts/mergeCoverage.js"
}

To run the script individually, you can always directly execute it in your terminal:

$ node ./scripts/mergeCoverage.js

All the relevant code is here, besides the above script: https://github.com/bahmutov/cypress-and-jest