All bytes
ProgrammingSunday, 07 June 2026 · 13:16 EAT

Test the behaviour, not the implementation

Tests that assert on implementation details break for the wrong reasons. Assert on the outcome the caller cares about, not how the function got there.

When tests assert on internal state, method calls, or intermediate values, every refactor breaks them — not because the behaviour changed, but because the implementation did. The test suite becomes a blocker rather than a safety net. The team starts dreading greenfield refactors because the tests, which were supposed to enable change, now prevent it.

The fix is to test what the function guarantees to its callers: return values, side effects the caller depends on, error states it promises to handle. Implementation details are free to change. A good test survives a complete rewrite of the function body, as long as the contract with the caller is preserved. This constraint forces you to think about the interface design before you write either the code or the test — which is itself a net positive.

This is harder than it sounds because the implementation is right in front of you while the contract requires abstraction. But the payoff compounds: a test suite that breaks only when the product's observable behaviour changes becomes trusted enough to run on every commit. Untrusted tests are worse than no tests — they teach engineers to ignore red builds.

Takeaway

A test that breaks on refactor tests the wrong thing.