Skip to content
Back to Ideas 6 min read

The Best Coverage Number in the Room

We have been telling cohorts to stop mocking for two years, which is easy to say and hard to prove. So last winter we started measuring our own codebase instead of asserting things about everyone else's, and we wrote the numbers down as we went, including the ones we did not like.

The cleanest measurement is a pair. Two branches sit on the same production commit, cover the same behaviour specification, and run almost the same number of tests, 71 against 70. One uses vi.mock at the collaborator boundary. The other runs sociably against a real database. Nothing else differs. Whatever separates them is the technique and only the technique.

What came out

MockedSociable
Tests7170
Wall clock2.29 s12.80 s
Line coverage14.95%18.47%
Function coverage65.95%72.72%
Branch coverage79.16%74.19%

Read the first three rows and the story is the one you expect. Mocks are 5.6 times faster and execute 3.5 fewer points of the codebase. The mechanism is not subtle. The mocked push and check run tests replace the repository service, the Prisma client and the XP service with stubs. Those functions never run, so they never count. A mock is, by definition, a decision not to execute the thing you are mocking, and a coverage tool is a machine for noticing exactly that.

Now read the last row again.

The number that flatters

The mocked suite posts the highest branch coverage of every condition we measured. A team reading that off a dashboard would conclude their tests were in excellent health, and they would be badly wrong.

Branch coverage only measures branches inside code that executes. The mocked suite runs a small, richly branched slice of the system, the parsing, the pure logic, the handler control flow, and it runs that slice very thoroughly indeed. Meanwhile most of the data layer never runs at all, so its branches are not scored as missed, they are simply absent from the question. Restrict the exam to the chapter you revised and your mark goes up.

This is the part worth taking to your own project. A single high coverage headline can rise as your tests touch less of the system, because the metric measures thoroughness within whatever ran, and mocking is how you shrink what runs. Line and function coverage told the truer story for us. One number on its own told a flattering lie.

The trade nobody advertises

Put the two halves together and the shape is uncomfortable: mocks are much faster because they do not run your code, and they cover less because they do not run your code. Speed and executed fidelity are not independent dials you can tune separately. For mocks they move together, in opposite directions from the one you want.

That is the honest case for mocks, incidentally. If a suite exists to give you a fast red or green on pure logic, mocking the world around it is a rational trade. The failure is not choosing speed. The failure is choosing speed and then reading the coverage number as though you had chosen fidelity.

What we did about it

The Nullable pattern is a claim that you can have both: infrastructure wrapped in a thin class that knows how to build a version of itself with only its lowest level switched off. The database, the clock and the network answer to the test, and every line of your own code between the call and the boundary runs for real.

We have been converting the dojo to it, commit by commit, with a measurement point at each step. Today's suite is 55 test files, 644 tests, and 95.98% line coverage, against the 35 tests and 9.34% we started with. That is the good half of the sentence.

The other half: it takes about four minutes. The suite grew slower as it grew truer, which is not the outcome the pattern promises, and we have not yet done the work of separating "narrow integration tests are inherently slower" from "we have been careless about where the database is touched". Getting it back to seconds without giving back the fidelity is the next thing we measure, and when we have the number, it will go in the notebook whichever way it falls.

The number for today

Same code, same specification, same test count: 5.6 times faster, 3.5 points less of the codebase executed, and the best looking branch coverage in the room belonging to the suite that ran the least.

Next in this series: the twenty six tests we gained on the way out of mocks, and the four behaviours we lost without noticing.

This way of working is how we teach everything, and the nearest course in this stack is API Design in Node.js with TypeScript: four weeks, two evenings a week, one real API built test first with no mock anywhere, with the measurements above handed over so you know what it costs before you spend it.

More from Ideas

·8 min read

Inversion of Control Doesn't Need a DI Container

We started designing a dependency scanner, and its needs led us to Inversion of Control. It turned out we were already doing it, without a DI container.

A
Anastasios Orfanidis
·7 min read

Unknown Is an Answer

A stub that holds no documents could not say whether a change still matched a query. Letting it answer unknown named a case the real database has too.

·7 min read

The Inbox That Delivers Nowhere

Signing in by emailed code puts a mailbox on the critical path. How we use Mailpit on Studomia, how our tests read the code, and what one question taught us.