How to Combine Coverage from Cypress (E2E and Component Test) and Jest in an Nx Project

Share on facebook
Share on google
Share on twitter
Share on linkedin

Coverage helps us see what part of our app is covered with tests and can help us determine if new code is covered in pull requests with tools such as SonarQube or Coveralls. My recommended testing strategy for testing Angular apps is to focus on covering the use cases with Cypress component tests and add some unit tests around edge cases and calculation while having a few e2e tests as a smoke test thus needing to combine test coverage from all these different sources to get a clear picture of what code is tested.

Getting coverage for Jest comes out of the box but setting up coverage with Cypress E2E and component tests and merging the coverage results all together requires more manual intervention and this blog post is covering how to get coverage from the different testing techniques and combine the coverage results into one report in an Nx project.

How coverage works

Before we dive in, let’s cover how code coverage is calculated in the first place.

A process known as instrumentation is performed on the code, adding counters for each statement to count when statements are executed. The tests would then run against the instrumented code and after that, the counters can be read from the different statements and conducted into a coverage report.

To do the instrumentation, it’s most commonly done with a webpack/babel loader such as @jsdevtools/coverage-istanbul-loader. Alternatively, the instrumented code can be generated with the NYC the command line interface for the coverage tool Istanbul.

Setting up coverage for Jest tests

Setting up coverage with Jest is the most straightforward as it is covered out of the box.

In the Jest config, we just set the properties for enabling coverage, so it becomes:

export default {
  displayName: 'jest-cypress-coverage',
  preset: './jest.preset.js',
  setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
  coverageDirectory: './dist/coverage',
  coverageReporters: ['json', 'text', 'cobertura', 'lcov'],
  transform: {
    '^.+\\.(ts|mjs|js|html)$': [
      'jest-preset-angular',
      {
        tsconfig: '<rootDir>/tsconfig.spec.json',
        stringifyContentPathRegex: '\\.(html|svg)$',
      },
    ],
  },
  transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
  snapshotSerializers: [
    'jest-preset-angular/build/serializers/no-ng-attributes',
    'jest-preset-angular/build/serializers/ng-snapshot',
    'jest-preset-angular/build/serializers/html-comment',
  ],
  testMatch: [
    '<rootDir>/src/**/__tests__/**/*.[jt]s?(x)',
    '<rootDir>/src/**/*(*.)@(spec|test).[jt]s?(x)',
  ],
};

In particular, we are setting coverageDirectory and coverageReporters.

Then we make sure to run our jest tests with coverage by setting the coverage flag in the Nx project.json:

    "test": {
      "executor": "@nx/jest:jest",
      "outputs": ["{workspaceRoot}/coverage/{projectName}"],
      "options": {
        "jestConfig": "jest.config.ts",
        "codeCoverage": true,
        "passWithNoTests": true
      },
      "configurations": {
        "ci": {
          "ci": true,
          "codeCoverage": true
        }
      }
    },

And we can now run nx test and we will see the coverage files being generated:

Setting up coverage for Cypress component tests

For Cypress component test coverage we need a bit more manual configuration. We will need to use a custom webpack config that use the loader: @jsdevtools/coverage-istanbul-loader for instrumentation.

The webpack config looks like this:

import * as path from 'path';

export default {
  module: {
    rules: [
      {
        test: /\.(js|ts)$/,
        loader: '@jsdevtools/coverage-istanbul-loader',
        options: { esModules: true },
        enforce: 'post',
        include: [path.join(__dirname, 'src')],
        exclude: [
          /\.(cy|e2e|spec)\.ts$/,
          /node_modules/,
          /(ngfactory|ngstyle)\.js/,
        ],
      },
    ],
  },
};

The webpack config is consumed by builder @angular-builders/custom-webpack:browser which allows us to extend the Angular webpack config.

First, we install the custom webpack builder:

npm i -D @angular-builders/custom-webpack

We set up Cypress component tests and generate component tests files with:

nx generate @nx/angular:cypress-component-configuration --project=jest-cypress-coverage

Lastly, we need to configure our Cypress config to run tests on the instrumented code and generate our coverage reports.

For Cypress to generate a coverage report we need to use the @cypress/code-coverage package.

We install it with: npm i -D @cypress/code-coverage.

We add this to our component.ts file:

import './commands';

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

And this to our cypress.config.ts file:

import { nxComponentTestingPreset } from '@nx/angular/plugins/component-testing';
import { defineConfig } from 'cypress';
import coverageWebpack from './coverage.webpack';

const nxPreset = nxComponentTestingPreset(__filename);

export default defineConfig({
  component: {
    ...nxPreset,
    devServer: {
      ...nxPreset.devServer,
      webpackConfig: coverageWebpack,
    },
    setupNodeEvents(on, config) {
      // eslint-disable-next-line @typescript-eslint/no-var-requires
      require('@cypress/code-coverage/task')(on, config);

      // It's IMPORTANT to return the config object
      // with any changed environment variables
      return config;
    },
  },
  includeShadowDom: true,
});

And we can run our component tests like nx run component-test and see the generated coverage reports

Setting up coverage for Cypress E2E tests

Setting up coverage for E2E tests is similar. We will also set up the instrumentation with the custom webpack extension including the same Istanbul loader as for component tests and we will consume them in project.json by serving the app using the @angular-builders/custom-webpack:browser executor.

In the e2e project.json we will have:

"e2e": {
  "executor": "@nx/cypress:cypress",
  "options": {
    "cypressConfig": "e2e/cypress.config.ts",
    "devServerTarget": "jest-cypress-coverage:serve-coverage",
    "testingType": "e2e"
  },
  "configurations": {
    "production": {
      "devServerTarget": "jest-cypress-coverage:serve-coverage"
    }
  }
},

And likewise, we are configuring Cypress to generate the coverage results.

We config the e2e.ts file like this:

import './commands';

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

And our cypress.config.ts is configured like:

import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
import { defineConfig } from 'cypress';

export default defineConfig({
  e2e: {
    ...nxE2EPreset(__dirname),
    setupNodeEvents(on, config) {
      // eslint-disable-next-line @typescript-eslint/no-var-requires
      require('@cypress/code-coverage/task')(on, config);
      return config;
    },
  },
});

And then we can run nx e2e e2e and see the generated result:

Merging the coverage results

Now all of our different testing methods can generate individual coverage results but we would like to merge them so we can upload combined result to a coverage tool like SonarQube or Coveralls.

We will do this by copying the individual coverage results to the same folder and use the NYC (instanbul CLI) tool to merge the coverage results.

We will write a complete script to copy the individual coverage results, merge the results and generate the combined coverage report.

/**
 * 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 = 'combined-coverage';

const { program } = require('commander');

program
  .option(
    '-e --e2e-cov-dir <dir>',
    'Directory for e2e coverage',
    'e2e/coverage'
  )
  .option(
    '-c --ct-cov-dir <dir>',
    'Directory for cypress-ct coverage',
    'coverage'
  )
  .option(
    '-u --unit-cov-dir <dir>',
    'Directory for unit test coverage',
    'dist/coverage'
  );

program.parse();
const options = program.opts();

console.log('Running merge with options:', options);

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(
  options.ctCovDir + '/coverage-final.json',
  `${REPORTS_FOLDER}/from-cypress-ct.json`
);
fs.copyFileSync(
  options.unitCovDir + '/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
  `npx nyc merge ${REPORTS_FOLDER} && mv coverage.json .nyc_output/out.json`,
  `npx nyc report --reporter lcov --report-dir ${FINAL_OUTPUT_FOLDER}`,
]);

This can then be run as:

node ./tools/scripts/merge-coverage.js

And we get the combined coverage report:

We can now see how our app is covered with unit, component and e2e tests!

Thanks to this combined report we see that todo-list needs some more tests…

Demo repo

A full demo of this can be found on my Github here.

Conclusion

In conclusion, by setting up coverage for Jest, Cypress component tests, and Cypress E2E tests, and combining the coverage results, you can gain insights into the test coverage of your Angular app. This enables you to make informed decisions about the quality of your codebase and ensure that new code is adequately covered by tests.

If you want to learn more about testing and my high ROI testing strategy, I recommend you sign up for my training in the next cohort here.

Do you want to become an Angular architect? Check out Angular Architect Accelerator.

Related Posts and Comments

How I migrated my Course Platform to Analog (step by step)

Analog is a full-stack framework for Angular ala NextJS that offers server-side rendering, static site generation, and API routes. Analog empowers Angular with server rendering tools making it optimal for public websites. Otherwise, Angular has often been neglected in favor of NextJS/NuxtJS for these purposes (due to the subpar SSR/SSG experience). I recently migrated my

Read More »

The Future of Angular and the Latest Features in Angular

In this video, I’ll be discussing the future of Angular and the latest features in Angular 17, including standalone components, signals, new control flow, the deferred operator, and server-side rendering improvements. I’ll also touch on the use of Tailwind CSS for styling and the benefits it offers. Join me to learn more about the exciting

Read More »

How to Invest as a Software Developer

In this video, I share my personal investment strategy as a software developer, focusing on putting money to work and owning assets that generate cash flow and appreciation. I discuss the snowball effect of building an investment portfolio over time and the importance of compounding. I also touch on the allocation of investments in stocks,

Read More »

Angular 17: What’s new?

Angular has since the latest major version started what the Angular team calls a renaissance, which means a radical renewal of the framework. The main goals of the latest updates have been to improve the developer experience and performance so it aligns more with the other leading front-end frameworks in the space by introducing new

Read More »

How to Set up a CI pipeline with Azure Pipelines and Nx

It goes without saying that having a CI pipeline for your Angular apps is a must. Setting one up for regular Angular apps is fairly straightforward but when you have an Nx monorepo there are certain other challenges that you have to overcome to successfully orchestrate a “build once, deploy many” pipeline. This post will

Read More »

How to Handle Errors in a Reactive Angular App

In this post, we will cover how to handle errors in a reactive Angular app. To provide a good user experience you should always let your users know what state your application is in. That includes showing a loading spinner when it’s loading and showing error messages if there are any errors. It is a

Read More »