← Concepts

阅读中文版本

Characterization Tests

Characterization tests capture what legacy code actually does — including its bugs — as a safety net for refactoring, pinning current behavior before changing it.

testing

Definition

A characterization test asserts the current observed behavior of code, not the specified or desired behavior. They are written when touching legacy systems with no tests: record what happens for representative inputs, lock it in, and refactor underneath the net. Bugs become pinned facts, marked for later correction as separate changes.

Context and scope

Used in legacy rescue operations before refactoring or migration. Michael Feathers working effectively with legacy code defines them as the tool for putting seams under untested code.

Common misconceptions

  • Characterization tests verify correctness — they verify stability; asserting observed behavior including defects is the point
  • Write them for the whole system first — characterize the slice you are about to change; broad nets take longer than the refactor
  • They become permanent regression tests — many are scaffolding to be deleted after the refactor lands

Anti-patterns

  • Fixing bugs while characterizing and never knowing whether the refactor or the fix changed behavior
  • Characterizing only happy paths and refactoring error handling blind
  • Keeping thousands of characterization tests forever and inheriting a maintenance backlog from the rescue

Related concepts

Related examples

Authoritative source

https://martinfowler.com/bliki/CharacterizationTest.html

What it is

Legacy code is code without tests, and changing it unverified is archaeology with a hammer. Characterization tests turn observed behavior into executable facts so that refactoring has a tripwire.

How it works

Choose the seam you will change. Feed representative inputs, capture actual outputs, and write assertions against them — including the surprising ones. Mark bug-pinned assertions explicitly so they are fixable later as deliberate behavior changes. Refactor in the net; delete the scaffolding tests that no longer earn their keep.

When to use them

Before any structural change to untested code: dependency extraction, performance rewrites, framework migration. The alternative — refactor freehand and eyeball the diffs — is how behavior silently changes during cleanups.

Pitfalls

Over-pinning: characterizing incidental details freezes quirks into contract. Characterizing at the wrong level — implementation internals rather than observable boundaries — makes every legitimate refactor trip the net. Snapshot-style bulk characterization amplifies both problems.