Achieving 100% Test Coverage — Node.js Edition
Lessons learned and best practices
So you just finished writing your Node.js app, complete with enterprise grade technologies like TypeScript. Your code is formatted with Prettier, and ESLint is configured to use a modern ecmaVersion and extends recommend rules.
Surely with no red squiggles in sight your code should be ready for production right?

Many developers omit writing tests for a variety of reasons, myself included, but in reality they are a godsend. In a production setting they give you confidence about implementation, and in a team setting they document functionality and enable rapid iteration.
The feedback loop is simple: Introduce new code that breaks tests? Fix it! All tests pass and code coverage is 100%? Good to go!

If you would like to opt out of reading the journey and dive right into the good stuff, feel free to jump to Lessons Learned
A Paradox of Speed
For the longest time I ignored writing tests because I had achieved what I felt like was ultimate coding velocity. Write code. Works? Great. Try to break it. Breaks? Fix it. Fixed? Push to prod! This worked well for me because the break-it-fix-it loop gave me great intuition about what could break in a codebase. Typo? Learn how to configure ESLint so it catches it while coding. Missing param? ESLint handles that. Will my class / library / function run into errors? Strict TypeScript rules hold your hand through that.
This feedback loop was further enforced by full-stack development. Why do I need API tests when I am writing the frontend right as I finish the API? The implementation works as intended and the happy path is well defined. As long as there is sufficient error handling and the UI has hints about how the feature should be used, I should be set!
However, as I added more features to my side projects, and as I began working on larger teams with more complex codebases, I began to feel overwhelmed keeping track of how all of the code worked, trying to sort out how the code I would introduce could affect other parts of the codebase.
Paralyzed from uncertainty, many codebases felt like the following picture: Bright and well defined in places I’ve explored but dark everywhere else.

Figuring it Out

I had always seen these badges on projects, but I never bothered to look at how it was achieved. I finally bit the bullet and spent a few days reading through tests of popular web frameworks like express, koa, and fastify, a few libraries like ioredis and kafkajs to get an idea of how they did testing.
The more I read, the more patterns I recognized. For example, object deep equal and snapshot testing are useful to ensure the shape of a newly constructed object or response matches a previously known valid state. Asynchronous tests make it easy to create tests that mimic real-world scenarios. Kafkajs for example includes a test that awaits a connect function, publishes some events, and then awaits a clean disconnect.
As I started to build my arsenal of testing patterns, the more confidence I started to have to write tests of my own. And as I began writing more and more tests, the easier they became. After a while, writing tests became second nature and my habit of build-it-break-it-fix-it became code-it-test-it-fix-it.
Nowadays, you’ll be hard pressed to find a new JS project of mine that doesn’t meet 100% test coverage.
Lessons Learned
- Go fast by going slow. The sooner I internalized the code-test-fix loop the sooner those red and yellow uncovered line indicators became less scary.
- Time I spent writing tests are no where near the amount of time fixing bugs that made it to production or the time spent jumping through code blocks mapping out how things work.
- If my tests are hard to write, it means the architecture of my code may be too complicated and depend on more things than necessary.
- Jest is a mature framework with guides that cover many common use cases. They’re a treasure trove of testing knowledge.
- The community is huge and if core jest doesn’t do everything you need, chances are there’s a library that extends functionality to suit your needs.
Patterns
Below are two of the most useful patterns that I’ve come across.
- Mock Inner Functions — Ensure internal functions are being called by mocking them. Thrown errors can be caught and promise rejections can be expected.

- Mock External Dependencies
Package mocks like @shelf/jest-mongodb
allow you to use dependencies, in this case mongodb, in your tests without actually having it running.

However, sometimes a community maintained package won’t exist, or you may want a lighter package mock.

Thanks for reading, I hope you found something interesting — and if you have any critiques or recommendations or anything cool related to testing, please share!
If you enjoy content like this and would like to work on a team with talented engineers who celebrate growth, tweet at us on twitter @complicore or email us at hello@complicore.co!