In this article, we'll look at why testing in JavaScript applications is important. You'll also learn how to measure code coverage, how to maximize your investment in testing, and what the Node.js reference architecture recommends to ensure adequate code coverage.
Read the other articles in our Node.js reference architecture series:
- Part 1: Overview of the Node.js reference architecture
- Part 2: Logging in Node.js
- Part 3: Code consistency in Node.js
- Part 4: GraphQL in Node.js
- Part 5: Building good containers
- Part 6: Choosing web frameworks
- Part 7: Code coverage
- Part 8: Typescript
- Part 9: Securing Node.js applications
- Part 10: Accessibility
- Part 11: Typical development workflows
- Part 12: npm development
- Part 13: Problem determination
- Part 14: Testing
- Part 15: Transaction handling
- Part 16: Load balancing, threading, and scaling
- Part 17: CI/CD best practices in Node.js
- Part 18: Wrapping up
What is code coverage?
Code coverage is a software testing metric that determines how much code in a project has been successfully validated under a test procedure, which in turn, helps in analyzing how thoroughly software has been verified.
To measure the lines of code that are actually exercised by test runs, the code coverage metric takes various criteria into consideration. The following are a few important coverage criteria.
- Function coverage: The functions in the source code that are called and executed at least once.
- Statement coverage: The number of statements that have been successfully validated in the source code.
- Path coverage: The flows containing a sequence of controls and conditions that have worked well at least once.
- Branch or decision coverage: The decision control structures (loops, for example) that have executed properly.
- Condition coverage: The Boolean expressions that are validated and that execute both
TRUE
andFALSE
during the test runs. - Condition coverage: The Boolean expressions that are validated and that execute both
TRUE
andFALSE
during the test runs. - Condition coverage: The Boolean expressions that are validated and that execute both
TRUE
andFALSE
during the test runs.
What modules should you use for code coverage?
If you've read the previous installments in the Node.js reference architecture series, you know that we recommend modules that our teams are familiar with and use regularly. This article is no different. The teams defining the reference architecture have decided on two modules that we recommend for code coverage:
- nyc, probably the most popular tool for code coverage. One of the main reasons this module is the most popular is that it works well with most JavaScript testing frameworks.
nyc
is the successor command-line interface (CLI) foristanbul
. - Jest, which generates coverage when you run the tool with the
--coverage
option.
This article's examples use nyc
.
Testing example
Download the example from the Nodeshift example repository. The example is made up of two simple functions located in the index.js
file, and a test in the test
directory that uses the Mocha test runner.
The first function adds two numbers:
function addTwoNumbers(x, y) {
return x + y;
}
This function can be easily covered by this simple test:
describe('testing for coverage', () => {
it ('should add 2 numbers correctly', () => {
assert.equal(addTwoNumbers(1,1), 2);
});
});
Code coverage can be generated easily too: Just call the nyc
module in conjunction with our test to generate the coverage report.
The package.json
might look something like this to run our tests while generating coverage reports:
"scripts": {
"test": "mocha",
"coverage": "nyc npm run test"
}
Executing npm run coverage
is all that is needed. Because we wrote only one test, our statement coverage will be only about 18%. To increase code coverage, we have to test the other functions and statements:
testing for coverage
✔ should add 2 numbers correctly
1 passing (3ms)
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 18.18 | 0 | 50 | 18.18 |
index.js | 18.18 | 0 | 50 | 18.18 | 9-23
----------|---------|----------|---------|---------|-------------------
Once the other tests are written and run, code coverage should be at 100%:
testing for coverage
✔ should add 2 numbers correctly
✔ should test that phish is the best band
✔ should test the beatles are a good band too
✔ should test that nickelback is not that good
✔ should test that all other bands are pretty good
5 passing (6ms)
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
index.js | 100 | 100 | 100 | 100 |
----------|---------|----------|---------|---------|-------------------
What to cover
After much discussion, the reference architecture team's guidance on what to cover is broken down into two sections:
- Coverage for modules: It is important to cover all public APIs. Basically, any exposed API that a user can interact with should be covered.
- Coverage for applications: This coverage should be driven by the key user stories and key paths that the application can take. Unlike a module, here it is much more important to test and cover paths that are critical for your application than to worry about covering all paths.
Acceptable coverage thresholds
We realized in our discussions that not all projects are created equal. Some projects might be starting from scratch, whereas others contain legacy code bases.
For new projects without legacy code, a good percentage threshold is about 70%. This is because, with new projects, it is relatively easy to add tests while creating the application or module.
It might be a little harder to add coverage to a project that is older and doesn't have any coverage yet, because adding tests to old code with technical debt can be a challenge, especially for someone new coming into the project. In this case, a good percentage threshold is about 30%.
It is also our experience that when adding coverage to an older code base, focusing on the key user stories gives you the best return on your investment. Focusing on the path and branch/decision coverage can also maximize the investment required to get to 30%.
Guidance for open source projects
For free and open source projects, it might be helpful to post the results of the coverage to an external service, such as Coveralls. Creating issues that suggest ways to increase code coverage could be a way to attract contributors to the project.
It is also common to report the coverage increase or decrease percentage during a pull request or merge request CI run. In our experience, it is best to use this information in a code review rather than by blocking a merge.
What's next?
We cover new topics regularly as part of the Node.js reference architecture series. In the next installment, we discuss our experience with using both plain JavaScript and TypeScript.
We invite you to visit the Node.js reference architecture repository on GitHub, where you'll see the work we've already done and the kinds of topics you can look forward to in the future.
To learn more about what Red Hat is up to on the Node.js front, check out our Node.js topic page.
Last updated: January 9, 2024