Curious Pandas

Pin what it does before you change what it should do

Before you can safely change legacy code, you need tests - and the first ones should assert what the code does today, bugs included. Meet seams and characterization tests, and find out how an approval test can quietly stop being able to fail.

16 min+40 XP for finishing

The previous lesson argued for changing a system in place instead of rewriting it. That raises the obvious problem: legacy code is often code nobody dares to touch, precisely because there’s nothing to tell you when you’ve broken it. This lesson is about building that safety net first.

Feathers’s three ideas

Michael Feathers’s Working Effectively with Legacy Code (2004) gave the field its vocabulary. The seam definitions below are his words, from the book’s published excerpt; the other two are paraphrases, as summarised by others:

  • Legacy code is code without tests. Not old code, not bad code: code you can’t change with confidence.
  • A seam: “A seam is a place where you can alter behavior in your program without editing in that place.” Passing in a different object, or linking a different implementation, are examples. And: “Every seam has an enabling point, a place where you can make the decision to use one behavior or another.”
  • A characterization test records what the code actually does today, whether that’s right or not. You aren’t specifying correct behaviour yet. You’re pinning current behaviour, so a refactor that changes it by accident goes red.

That last point feels wrong at first. A test that asserts a bug? Yes, on purpose. Fixing the bug becomes a deliberate change with the test updated alongside it, instead of something that happens by accident in the middle of a refactor.

Approval tests: characterization at scale

When the output is big - a report, a page of HTML, a large JSON document - writing assertions by hand is impractical. Approval testing captures the whole output instead. The first run writes a received file; you read it and, if it’s what the code does, save it as the approved file. From then on, the test fails whenever the received output differs from the approved one. The tool is open source and has ports for many languages.

The same idea is often called a golden master test, and it has a well-known way to go wrong. When the test goes red and you’re confident the new output is right, it’s tempting to re-approve it without reading the difference. Do that once with a bug in the output, and the test now asserts the bug. J. B. Rainsberger’s advice is to make regenerating the golden master a deliberate, separate step, never the reflex when a test fails.

Try it

Three approval tests. For each, decide what it can still catch.

Can this test fail?
  1. The approved file was regenerated

    A refactor changed the output of a report and the approval test went red. A developer, sure the new output was right, re-approved it without reading the diff. It was a bug.

    Your reading

    Source: J. B. Rainsberger, How not to write golden master tests

  2. A generous scrubber

    The output contains timestamps, so a scrubber replaces them before comparing. To be safe, it replaces every digit in the file.

    Your reading

    Source: Inference from ApprovalTests scrubbers; compare the Strangler Lab's "ignore all numeric fields"

  3. A test that pins a known oddity

    Legacy code rounds half-pennies down. A new characterization test asserts exactly that, with a comment: "current behaviour, not necessarily correct - see ticket".

    Your reading

    Source: Michael Feathers, Working Effectively with Legacy Code (2004), as summarised

Check yourself+10 XP

A characterization test asserts that legacy code rounds half-pennies down, which you think is wrong. Why keep it?

Check yourself+10 XP

An approval test went red after a refactor. You're sure the new output is right. What should you do before re-approving?

Sources

Finished reading?

Missions and quizzes above earn their own XP. This marks the lesson done.