Why We Add Tests Before Changing Code

Our non-negotiable approach to adding coverage to undocumented legacy systems — and why it saves time instead of costing it.

Every legacy system we're asked to touch has the same story: it works, nobody remembers exactly why, and there's no test suite to explain the intent. The business logic lives entirely in the code's behavior, not in any document. When that's the situation, we have a hard rule: before we change a single line, we write tests that pin down what the system currently does.

The temptation to skip it

It's easy to argue against this. The client wants a fix shipped today, not a testing exercise. The bug looks obvious — a null check here, an off-by-one there — and writing tests first feels like ceremony standing between us and the actual work. We understand the temptation. We've felt it. We don't act on it anyway.

What "undocumented" actually means

When a system has no tests and no design docs, the only source of truth is the running code. That means every assumption you make about "what this is supposed to do" is a guess dressed up as confidence. You read a method, form a mental model, and start editing based on that model — except your mental model is built from a five-minute skim of code that took someone else months to get right, including edge cases you can't see just by reading.

Tests written against the current behavior turn that guesswork into something concrete. They capture what the system actually does, quirks and all, before you touch anything. If a quirk turns out to be a genuine bug, you now have a documented, deliberate decision to change it — not an accidental side effect discovered by a customer three weeks later.

Why this saves time, not costs it

The obvious objection: writing tests takes time you don't have. In our experience it's the opposite, for a simple reason — the cost of a regression in an undocumented system is enormous. Nobody remembers the reasoning behind the original code, so nobody can quickly tell whether new behavior is a bug or an intentional improvement. Diagnosing that after the fact means re-deriving context that a test would have made explicit from the start. That's expensive: production incidents, urgent debugging sessions, and eroded client trust cost far more than the hour it takes to characterize the existing behavior up front.

There's a second, quieter payoff: writing the tests is often how we find the bug. Walking the code path deliberately, asserting on outputs, and comparing them against what the code should do surfaces problems faster than reading the code start to finish ever does.

How we apply this

  • We write characterization tests against current behavior first — not the behavior we think is correct, the behavior that's actually there.
  • We treat any mismatch between "what the code does" and "what it should do" as a decision point, not something to quietly correct as a drive-by.
  • We keep the tests. They become the documentation the system never had, and the safety net for the next change.

This isn't extra process for its own sake. It's the only way to change code you don't fully understand without gambling on what breaks. On legacy systems, that gamble is the expensive part — not the tests.

Have a legacy system that needs modernizing?

Let's talk about your situation — no commitment, no sales pitch.

Get Free Assessment