The problem characterization tests solve
Refactoring and code-review rounds routinely hand you code with no tests, and no reliable specification — the only description of what it's supposed to do is what it currently does, weird edge cases included. Writing tests against your own idea of what the code should do, before you've confirmed what it actually does, is a common and costly mistake here: you can end up "fixing" behavior that some caller elsewhere silently depends on, and calling that a bug when it was actually load-bearing. Michael Feathers coined the term characterization test for exactly this situation, and defined its purpose precisely: "to document your system's actual behavior, not check for the behavior you wish your system had."
The algorithm, and why it works even when you don't understand the code yet
Feathers' process is deliberately mechanical, which is exactly what makes it usable under time pressure on code you've never seen:
- Put the code under test in a harness — call it with some concrete input, even a somewhat arbitrary one to start.
- Write an assertion you expect to fail — a placeholder value, since you don't yet know the real output.
- Run the test and read the actual output from the failure message.
- Update the assertion to match that actual output. The test now passes, and it's now documenting real, current behavior.
- Repeat with more inputs — especially boundary values, empty/null cases, and anything that looks like it might be handled specially — until you have enough coverage to feel safe changing the code underneath.
Every step is possible without understanding why the code behaves the way it does — you're building the tests first, and using them to build understanding, rather than the reverse. This is the inversion that makes the technique fast on unfamiliar code: you don't need a mental model before you start, the process of writing these tests builds one.
What to do with surprising behavior you discover
It's common, doing this seriously, to find behavior that looks wrong — an off-by-one, an inconsistent rounding rule, a case that silently does nothing when you'd expect an error. The correct move is not to "fix" it as part of writing the characterization test — that's exactly the untested, unverified change this technique exists to prevent. Lock the surprising behavior down as a passing test first (with a comment naming it as surprising, if you like — future-you or a reviewer will want the context), and treat "is this actually a bug, or an intentional-if-undocumented decision" as a separate, explicit question to raise with whoever owns the code, once you have a safety net that would catch you changing it either way.
Characterization tests as the bridge into safe refactoring
Once you have a reasonable characterization suite, you've converted "code with no tests" into "code with a regression suite" — which is exactly the precondition Safe Refactoring in Small Steps assumes. This ordering — tests first, changes second — is the single highest-signal habit in any round that hands you legacy or unfamiliar code and asks you to change it; skipping straight to changes because "it looks simple enough" is the most commonly cited way candidates lose points in exactly this kind of round.
A concrete way to practice this deliberately
The Gilded Rose Refactoring Kata is built for precisely this exercise: a small inventory-aging system with genuinely surprising, undocumented rules (a "Sulfuras" item that never ages, an "Aged Brie" that increases in quality over time, a hard cap that isn't obviously stated anywhere) that you're meant to pin down with characterization tests before adding a new item type — attempting the new feature without that safety net first is the kata's most common failure mode, deliberately.
Further Resources (Optional)
Practice Problems
Interview relevancy:!!!Critical — must-have!!Important — highly recommended!Good to have~Niche — rarely asked
- Gilded Rose Refactoring Kata — pin down the existing item-aging rules with characterization tests before adding a new item typeGitHub Kata (Emily Bache)!!!3/545m