Mocks solve a real problem. You want to test your code in isolation, without a live database or a flaky third-party API or the slowness of real I/O, so you replace the dependency with a stand-in that returns what you tell it to. The test gets fast, deterministic, and focused on your logic. This is genuinely useful, and for pure unit tests of your own decision-making it’s often exactly right. But it comes with a quiet substitution that’s easy to forget: you didn’t test against the dependency. You tested against your belief about the dependency, encoded in the mock.

That’s fine as long as the belief is correct, and the trouble is that beliefs about other people’s systems are wrong all the time in small ways. You mock the API to return a clean JSON object with the fields you expect. In reality it sometimes returns those fields as strings instead of numbers, or omits one under a condition you didn’t know about, or returns a 200 with an error body. Your code handles the mock’s version perfectly because the mock is the version your code was written against — they were authored by the same person with the same assumptions. The test passes not because the code is right but because the mock and the code agree, and they agree because they came from the same misunderstanding.

This is what makes mock-heavy test failures so disorienting when they finally happen in production. Every test is green, the logic looks correct, and the bug is in the seam between your code and something real — precisely the seam every mock was designed to remove from the test. You mocked away the exact interaction that broke. The more thoroughly a suite isolates each unit behind mocks, the more completely it stops testing the thing that integration actually is: whether these pieces, as they really behave, fit together. You can have a hundred passing unit tests and zero evidence that the system works, because “the system” is mostly the connections, and the mocks deleted the connections.

The correction isn’t to abandon mocks — testing everything against live dependencies is slow, flaky, and sometimes impossible. It’s to be honest about what each test proves and to make sure something, somewhere, exercises the real seams. A few integration tests that hit an actual database or a real sandbox of the API catch the entire class of “my mock was wrong” bugs that no amount of unit testing can. Contract tests help too: verifying that your mock’s assumptions actually match the provider’s current behavior, so that when they change their response shape, a test fails instead of production. The mock is a fixture of a moment; the real thing keeps moving, and something has to keep checking that the fixture still matches.

The deeper caution is to notice when a mock has stopped standing in for reality and started standing in for what you wish reality were. A mock you wrote to match your understanding will, by construction, confirm your understanding — it can’t tell you you’re wrong about the thing it’s imitating, because you built it from the same assumptions you’re trying to test. That’s the failure mode that connects to the coverage problem: a test that can’t fail for the bug you actually have provides confidence without providing safety. With coverage the culprit is missing assertions; with mocks it’s assertions checking a world you invented. Both feel like testing. Only one of them would have caught the thing that took you down.