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
executor.@angular-builders/custom-webpack:browser
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.