Skip to main content

Characterization Tests: Pin Behavior Before Change

What This Concept Is​

A characterization test (Michael Feathers) is a test whose purpose is to capture what the code actually does, not what it should do. You write one before refactoring legacy code whose behavior is not documented.

Flow:

  1. Guess an input the code is likely to handle.
  2. Run it, observe the output.
  3. Write an assertion that locks in that observed output as the expected value.
  4. Repeat for edge inputs and error paths.

The test suite then becomes the baseline. Only changes exercised by the selected inputs and assertions are detectable. A changed deadline comparison can survive tests that never hit equality.

Characterization tests often include current bugs. That is deliberate. Record any proposed repair separately. Urgent defects may need fixing first; preserve the original observation and explicitly change the expected behavior before structural work.

Why It Matters Here​

Every refactor move in this module assumes "tests stay green after each step." When automated coverage is missing, characterize a public boundary before risky transformations. A controlled tool transformation or formal argument may supply other evidence, but absence of tests increases uncertainty; do not claim the code is safe merely because it compiles.

Fowler emphasizes self-testing code as a practical feedback mechanism. That recommendation does not turn a passing finite suite into a proof of equivalence.

Concrete Example​

Legacy function:

function quoteFor(customerId, quantity) {
let base = quantity * 10;
if (customerId.startsWith("VIP")) base *= 0.9;
if (quantity > 100) base -= 5;
return Math.round(base);
}

You do not know if the -5 is right. You do not care yet. Pin current behavior:

test('characterization: ordinary customer, small order', () => {
expect(quoteFor('C001', 3)).toBe(30);
});
test('characterization: VIP, small order', () => {
expect(quoteFor('VIP42', 5)).toBe(45);
});
test('characterization: VIP, large order', () => {
expect(quoteFor('VIP42', 200)).toBe(1795);
});
test('characterization: ordinary, large order', () => {
expect(quoteFor('C009', 150)).toBe(1495);
});

Now you can Extract Function, rename, move -- the four tests check those four cases. They miss many changes, including quantity-zero and threshold-boundary behavior.

Common Confusion / Misconception​

"But test 3 encodes a bug!" Yes. Characterization tests freeze the present, not the ideal. When you later fix the bug, you will update that specific test in the feature commit, and the diff will show exactly what behavior changed. That traceability is the point.

Characterization describes a test purpose; unit and integration describe scope. Characterization tests can remain valuable regression tests. Remove one only after identifying the observation covered elsewhere or the retired contract.

How To Use It​

Quick rules:

  • Prefer many small tests over one large one (easier to localize a red).
  • Capture snapshots for complex outputs; keep human-readable values when possible.
  • If the code talks to a network or clock, introduce a seam (next concept) before writing the test.

Check Yourself​

  1. Why is it acceptable for a characterization test to encode a bug?
  2. You run the code and the output is a 4KB JSON blob. What are two options for asserting it?
  3. What is the minimum number of characterization tests before you can start refactoring, and what determines it?

Mini Drill or Application​

Find a 30-line function in a codebase you do not own. Write three characterization tests: happy path, boundary case, error case. Run them green. Now Extract Function on the happy-path branch. Tests must still be green. If they go red, pause and isolate the cause. Revert the small step if useful; a structural test failure and a public behavior regression require different repairs.

Engineering Extension: Treat the oracle as code worth reviewing​

Differential agreement with the old implementation preserves its defects too. The kit pairs comparison with an independent rational arithmetic oracle and scalar state model. Keep intentional repair assertions separate from equivalence assertions. Normalize only nondeterminism explicitly excluded by the contract; sorting output can hide a real ordering regression.

Use the behavior contracts, labs and practice feedback to test the claim.

Read This Only If Stuck​

Locate this move through the Refactoring book index. The selective reading guide gives exact consulted local locators and primary documentation. Index links are not exact chapter links.