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 database | Nullable | Ratio | |
|---|---|---|---|
| Behaviours | 7 | 7 | same |
| Wall clock, mean of ten | 2.712 s | 0.837 s | 3.24x |
| Time inside the tests | about 1827 ms | 10 ms | about 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 port | After | |
|---|---|---|
| Line coverage | 21.00 % | 21.06 % |
| Branch coverage | 77.33 % | 77.33 % |
| Function coverage | 81.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.

