100% test coverage and a broken migration. Same week.
The suite was green. CI was green. The deploy was green. The migration ran on a Tuesday afternoon and dropped a column something was still reading from. Every test that would have caught it was mocked against a schema the fixture hadn’t updated since the quarter before last. The mocks still returned 200. The database didn’t.
Coverage measures what you ran. It does not measure what will happen.
A test that mocks the database tests your mock. A test that mocks the network tests your mock. If the mock drifts, the test passes against an imaginary system. The test still runs green. None of that tells you whether the thing works.
Integration tests against real infra cost more. They’re slower, flakier, harder to run on a laptop. That’s the feature, not the bug. The friction is the closest a suite gets to the actual thing happening. I’ll take fifty slow integration tests over five thousand mocked unit ones, every time, on anything that matters.
This isn’t an argument against mocks. Tests need to be fast, deterministic, runnable offline, that’s what mocks are for. It’s an argument against trusting them where trust doesn’t belong. Mock the clock, mock the random seed, mock the third-party API you don’t own. Don’t mock the database and call it tested.
Don’t mock the thing the bug will be in.
Test count is a lying metric. So is coverage. The honest question is, what would actually have to break, in the real system, for this test to catch it? If the answer is “the mock,” you didn’t test the system. You tested the mock.
