The Test That Broke for the Wrong Reason
You refactor a function — same inputs, same outputs, cleaner internals — and thirty tests go red. Nothing about the system’s behavior changed. What changed is the shape of the code, and those tests were watching the shape: asserting that a particular helper got called with particular arguments, that the internal sequence went in a certain order, that a private structure looked a certain way. They weren’t checking that the code was right. They were checking that it was written the way it was written the day the test was authored.
The immediate cost is the hour spent updating tests that found no bug, and that’s annoying but survivable. The real damage is slower and worse: it teaches people that red doesn’t necessarily mean broken. Once a suite has a reputation for failing during safe refactors, a failure stops being information and becomes an obstacle. People start updating assertions to match new behavior reflexively instead of asking whether the new behavior is right, and eventually a genuine regression walks through wearing the same costume as the hundred false alarms before it. A test suite’s power is entirely in the meaning of a failure, and implementation-coupled tests spend that meaning down to nothing.
There’s a second cost that’s harder to see: those tests make the code resistant to improvement. If any restructuring triggers a cascade of test edits, then refactoring carries a tax, and taxed things happen less. The suite that was supposed to make change safe has made it expensive instead — which is exactly backwards, because enabling confident change is the main thing tests are for. A codebase with brittle tests calcifies for the same reason a codebase with lost reasoning does: not because anyone decided to stop improving it, but because each individual improvement costs more than it appears to be worth.
The discipline is to test through the interface you’d defend, not the mechanism you happen to have. Assert on what the unit promises to its callers — return values, persisted results, messages emitted, errors raised — and stay silent about how it gets there. Then a refactor that preserves behavior preserves green, and a red test means something actually changed for someone. The useful check when writing an assertion: if I rewrote this function completely but kept its contract, would this test still pass? If not, it’s pinned to the implementation, and it will bill you for every future change.
That’s the thread here, and all three parts are the same mistake wearing different clothes. Coverage counts lines that ran rather than behavior that was checked. Mocks verify a world you invented rather than the one you integrate with. Implementation-coupled tests fail for changes that aren’t defects. In each case the suite looks like protection and isn’t, and the number of tests tells you nothing about it. The only question that matters is what a failure would mean — whether it reliably means something is genuinely wrong, and whether the things that would genuinely go wrong would reliably produce one. Everything else is bookkeeping.