- Instrumenting the code as a pre-processor step
- During the tests
- The coverage report
- Code coverage from multiple spec files
- Code coverage on CI
- 100% code coverage
- See also
In my previous blog post "Stub navigator API in end-to-end tests" I have shown a small web application that shows the current battery charge information. Then I have shown several Cypress end-to-end tests and found an edge case that caused the application to crash.
The crash was caused by the failure of the program src/index.js to account for the final - else
code path when dealing with the browser capabilities.
1 | var battery |
Our end-to-end test in spec file no-battery.js removes both navigator.battery
and navigator.getBattery
properties and reaches the final - else
path, causing the window.onload
to crash - the variable battery
remains undefined. If only we could see right away before writing no-battery.js
that this code path is a problem!
Instrumenting the code as a pre-processor step
Cypress users who think collecting application code coverage during end-to-end tests proposed several solutions. All solutions instrument the web application code during the build step, then save the collected code coverage data after the tests. Finally, yet another step generates coverage report from the saved data. Let's see how it can be done for our battery API demo application.
You can find this code in branch coverage-step in the demo-battery-api repository.
First, we will instrument our application code as a pre-processing step using IstanbulJS library and its command line wrapper nyc. I will install the following two libraries
1 | npm install --save-dev nyc@14 istanbul-lib-coverage@2 |
I have added the command to instrument application code in src
folder and place the instrumented code into build/src
. The cp
command copies the rest of the application files: HTML and styles.
1 | { |
If we execute npm run build
we will get the instrumented application in the build
folder. For demo purposes I pass --compact false
during instrument step to avoid minification. The output code looks like this (this is only a small part of the instrumented code):
1 | if (navigator.battery) { |
The instrumentation just inserts counters into the code, incrementing them for each statement (the cov_w7e0b4vcv.s[31]++
line) and if - else
branch (the cov_w7e0b4vcv.b[11][0]++
line). After the code finishes running, we can map the numbers from the object window.cov_w7e0b4vcv
back to the original source code to see which lines were covered.
During the tests
The application code has been instrumented, and we serve the build
folder during end-to-end tests, rather than the original src
folder. Here is what we need to do in the browser during each test:
- reset the collected coverage before tests begin
- merge the code coverage collected during each test with previously collected data
- generate a coverage report in desired format after all tests have finished
The above 3 things are controlled by the cypress/support/index.js file that gets bundled with each spec file automatically.
1 | /// <reference types="cypress" /> |
Each cy.task jumps from the browser context to the backend context in Cypress where we have the full access to the file system. Thus we can save the code coverage JSON file and execute nyc
to produce the report. Here is the cypress/plugins/index.js that keeps the code coverage object and generates the report:
1 | const istanbul = require('istanbul-lib-coverage') |
Let's run the instrumented tests. I will run just a single test that removes navigator.battery
property.
1 | context('navigator.getBattery', () => { |
The Cypress Test Runner shows the code coverage related commands.
Great, let us see the covered lines.
The coverage report
The saved code coverage object is just a large JSON file .nyc_output/out.json
like this
1 | { |
The object contains location of all statements, functions and branches in the original code, and the counters for how many times each item has been executed (for example the s
counters are for statements). The human coverage report is generated from the .nyc_output/out.json
file using npm script "report:coverage": "nyc report --reporter=html"
is placed in coverage
folder. It is a folder with a static site.
1 | coverage/ |
We can open the coverage/index.html
file and see that our test really has hit just a single if - else
line. Lines in red show the code NOT covered by the tests.
We can enable all tests and see more lines covered - and the only uncovered line is the third code path - else
when there is neither navigator.battery
nor navigator.getBattery
methods.
Tip: I prefer having a separate npm script to generate the code coverage report, because we may want to generate reports in different formats. For example we can generate a summary report and show it in the terminal: "report:coverage": "nyc report --compact"
1 | $ npm run report:coverage |
We can even fail the build if the code coverage (lines, statements, branches or per file) is below given threshold:
1 | $ npx nyc report --check-coverage --statements 100 |
Better write more tests.
Code coverage from multiple spec files
If we have more than a single test (spec) file, we need to be careful when we reset the coverage information. There are two different cases, depending on how the test runner is running:
- if the test runner is in the interactive mode using
cypress open
then we can reset the coverage before the tests. This works for a single spec file, or when running all specs using "Run all specs" button. - if the test runner is in the headless mode using
cypress run
, then each spec is processed separately. It is almost like executing a series of separate commands:cypress run --spec test1.js
,cypress run --spec test2.js
, etc. We cannot reset the coverage in that case - we risk destroying the information collected from the previous spec file. We need to reset the coverage before running Cypress. Luckily this is simple to do, here I am using npmprecy:run
script that automatically runs beforecy:run
script.
1 | { |
From the tests, we can pass isInteractive
flag to the task, and only reset the coverage file when isInteractive
is true
.
1 | before(() => { |
1 | module.exports = (on, config) => { |
Perfect.
Code coverage on CI
The generated HTML code coverage report can be stored on continuous integration server as a static artifact. I am running these builds on CircleCI at https://circleci.com/gh/bahmutov/demo-battery-api/tree/coverage-step using Cypress Circle Orb. After the test run, I store the coverage
folder as a test artifact. Here is the entire CI config file circle.yml
1 | # see https://github.com/cypress-io/circleci-orb |
Note that on CI we do not need to remove the code coverage folder .nyc_output
before starting cypress run
- because CI server automatically gets a fresh workspace folder.
You can see the artifact with each build. For example you can browse to the artifacts in the build #28
Click on index.html
to see the JavaScript coverage report
We have only a single JavaScript application file, open the report for index.js
by clicking on it.
100% code coverage
Hitting a 100% code coverage is a hard and thankless task. Code coverage is not the goal in itself, rather the gap in the code coverage helps me identify a missing test and an overlooked edge condition. In our case, the uncovered logical branch shows that we did not test what happens when the browser navigator
object does not have the battery API. Let's fix the issue and enable the previously crashing test.
1 | window.onload = function () { |
You need to rerun the instrumentation step again (npm run build
in my case). Enable the previously crashing test - and it is passing now!
Run all tests again - interactively using "Run all tests" button, or using npm run cy:run
and get the 100% code covered.
See also
- The example source code is in the branch coverage-step in the demo-battery-api repository
- "Stub navigator API in end-to-end tests" is the previous blog post explaining the end-to-end tests for this web application
- Code coverage Cypress repo issue #346
- Cypress.io code coverage guide