Back to Ideas 6 min read

What the Nullable Gave Back

E
EkoHacks Team
·

Two posts of bad news deserve a third with the answer in it.

So far the series has established a trade we did not choose and cannot argue with. Mocks run less of your code, which is why they are fast and why they cover little. Leaving mocks costs 5.6 times the wall clock, and the whole of that difference is the database. Those are the two ends of one dial.

The Nullable pattern is a claim that the dial is fake: that you can put the database behind something that answers instantly while every line of your own code still runs for real. This post is the first time we measured that claim on our own codebase rather than repeating it in a workshop.

The narrowest honest experiment

One test file. The WakaTime sync service, seven behaviours, production code untouched. The chain under test runs from syncWakaTimeActivity through awardXP to updateParticipantRank, the same way in both versions.

Before, the test seeded a cohort, a squad and a participant into real Postgres for every scenario, ran the service, and read rows back to assert. After, it wires a Nullable Prisma wrapper into the service, scripts the reads the code performs, and asserts on the writes it emits through output tracking. No database anywhere.

Both were timed on the same host on the same day: one warmup discarded, then ten timed runs.

Real databaseNullableRatio
Behaviours77same
Wall clock, mean of ten2.712 s0.837 s3.24x
Time inside the testsabout 1827 ms10 msabout 180x

Both ratios are true and they measure different things. The 3.24 is what a developer feels running this one file, and it includes roughly 0.8 seconds of vitest starting up, which is identical in both columns and is the floor the Nullable run is already sitting on. The 180 strips that fixed cost out and shows what the database actually cost: about 1.8 seconds of opening transactions, inserting fixtures, reading rows back and truncating, seven times over, against 10 milliseconds of scripted answers.

As more files are ported the startup cost amortises across all of them, so the realised refund travels from the 3x floor towards the 180x ceiling. That is the mechanism, and it is why we report both numbers rather than the flattering one.

The number that mattered most did not move

The reasonable worry about replacing a real database with scripted responses is that you have quietly reinvented the mock, stopped executing real code, and bought your speed with the same currency.

Before the portAfter
Line coverage21.00 %21.06 %
Branch coverage77.33 %77.33 %
Function coverage81.01 %81.01 %

Flat, to within noise. The reason is the whole point of the pattern. A mock replaces your collaborator, so your collaborator stops running. A Nullable replaces the database driver, which lives in node_modules and was never inside the coverage scope to begin with. Everything above that boundary, which is to say all of your application code, runs in full either way. The Nullable answers at the edge of the system rather than in the middle of it.

That is the trade the whole study set out to test, and on this file it holds: the executed fidelity the sociable suite bought, at most of the speed the mocks had.

Three things that keep it honest

It is one file. This is the first measured point of the condition, not the condition. The suite total moved from 15.46 seconds to 12.42, which is directional at best, since those two measurements were taken months apart. The matched pair above is same host, same day, same behaviours, and is the result to trust.

The port is not free to write. Assertions had to be rebuilt from reading state back to tracking output, and every database call the chain makes had to be scripted in the order the code makes them, because a configurable response is a one shot queue that throws when it runs dry. The speed is refunded. The authoring effort is spent up front, and anyone selling you this pattern without that sentence is selling.

The failure behaviours still need budgeting. Nothing here undoes the previous post. A Nullable scripted for the happy path is as silent about error paths as any other happy fixture. You script the failures deliberately or you lose them, whichever technique you land on.

The number for today

Same seven behaviours, same production code, the database swapped for a Nullable: about 180 times less time spent inside the tests, and coverage flat to the second decimal place.

We teach this conversion as a four week course, Testing Without Mocks in TypeScript, two evenings a week, on your own codebase, with every measurement in this series handed over so you can argue with it.

E

Written by

EkoHacks Team

More from Ideas

·6 min read

Twenty Six More Tests, Four Fewer Behaviours

Removing the mocks grew the suite from 44 tests to 70 and quietly deleted four behaviours, every one of them a failure path. Test count is not coverage.

E
EkoHacks Team
·6 min read

The Best Coverage Number in the Room

Same commit, same spec, same test count. The mocked suite ran 5.6 times faster, covered 3.5 fewer points of real code, and posted the best branch coverage.

E
EkoHacks Team
·7 min read

We Promised Not to Watch. Then We Needed to See.

Our privacy policy said no analytics, and it was true. Adding a page counter meant reopening that promise in public: what we chose, what we refused, one commit.

E
EkoHacks Team