A characterization test describes what a system does now. It runs existing code with known inputs and records outputs or side effects that can be checked again after a change. The result is a behavioural baseline. It does not prove that the captured behaviour is correct.
What the test records
The observable surface depends on the boundary under test. A function may return a value or raise an error. A service may write records, publish messages, call another system, or produce a file. Characterization testing selects observations that matter to callers and operations.
| Observation | Example | What a change can reveal |
|---|---|---|
| Return value | Calculated total or status | Changed rule or rounding behaviour |
| Error | Exception type or response code | Changed failure contract |
| State change | Database record or file | Added, missing, or altered side effect |
| Outbound interaction | Message or service call | Changed integration behaviour |
Incidental details make poor assertions when callers do not rely on them. Timestamps, generated identifiers, and unordered collections may need controlled inputs or comparison rules so the test detects meaningful differences instead of noise.
Why legacy systems need a baseline
In a legacy codebase, documented requirements may be incomplete or older than the running behaviour. Current code can also contain defects that users and integrations have learned to accommodate. A characterization test preserves the observation first so maintainers can investigate it separately from the structural change.
This makes the test different from a specification test. A specification states what the system should do. A characterization test states what it did when the baseline was recorded. If the observed result is wrong, the baseline can still expose when and how a later fix changes it.
Building the test around a change
A focused process is:
- Choose a boundary. Select the function, service, or workflow that the planned change can affect.
- Provide a known input. Control data and external dependencies enough to repeat the observation.
- Capture current behaviour. Record outputs, errors, and relevant side effects.
- Review the observation. Mark suspicious behaviour for a separate decision rather than silently normalising it.
- Run it through the change. Compare the refactored or replacement path with the baseline.
A legacy codebase audit helps choose high-value boundaries by connecting important workflows to code and integrations. Broad test coverage alone does not show whether those behaviours have useful assertions.
How it supports incremental migration
Refactoring relies on behavioural continuity while internal structure changes. Characterization tests provide evidence for that continuity where a specification is missing. Branch by Abstraction can use the same baseline to compare old and new implementations behind one contract.
The test remains useful only while its observation represents behaviour that must be protected or deliberately changed. Once requirements are confirmed, teams can replace broad snapshots with narrower specification tests where appropriate. During a staged legacy migration, the baseline can also define a retirement check: the new path produces the required result, the old path is no longer needed, and known intentional differences are documented before the legacy implementation is removed.