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

Error, loading, content…? Use this page pattern for your Angular apps

When developing Angular applications, it’s common for pages to transition through three key states: error, loading, and show content. Every time you fetch data from an API, your page will likely show a loading indicator first, and then either render the content successfully or display an error message if something goes wrong. This pattern is

Read More »

How to do Cypress component testing for Angular apps with MSW

In this post, we will cover how to do Cypress Component testing with MSW (mock service worker) and why it’s beneficial to have a mock environment with MSW. The mock environment My recommendation for most enterprise projects is to have a mocking environment as it serves the following purposes : * The front end can

Read More »

Handling Authentication with Supabase, Analog and tRPC

In this video, I cover how to handle authentication with Supabase, Analog and tRPC. It’s based on my Angular Global Summit talk about the SPARTAN stack you can find on my blog as well. Code snippets Create the auth client Do you want to become an Angular architect? Check out Angular Architect Accelerator.

Read More »

Leave a Comment

Your email address will not be published. Required fields are marked *