Java

Characterization Testing

Characterization tests document the actual behavior of existing code. Unlike unit tests that verify intended behavior, characterization tests capture current behavior—bugs and all. They're essential for safely modifying legacy code.

📋 At a Glance

AspectDetails
PurposeDocument existing behavior, detect regressions
ApproachTest what code DOES, not what it SHOULD do
CoverageFocus on code paths you'll modify
OutputTests as executable documentation

🎯 What You'll Learn

  • Writing characterization tests for legacy code
  • Golden Master testing for complex outputs
  • Testing without seams (when dependency injection isn't available)
  • Dealing with side effects in tests
  • Test coverage strategy for legacy systems

Production Story: The Refactoring Disaster

A team refactored a pricing engine without tests:

JAVA(19 lines)
Code
Loading syntax highlighter...
Result: 500 customers got wrong prices. The "clean" code had subtle differences:
  • Discount order changed (loyalty applied before category fee)
  • Rounding happened at different point
  • Edge case for null customer behaved differently
The fix should have been: Characterization tests FIRST:
JAVA(11 lines)
Code
Loading syntax highlighter...

🔬 Deep Dive

Pattern 1: Basic Characterization Test

JAVA(43 lines)
Code
Loading syntax highlighter...

Pattern 2: Systematic Exploration

JAVA(41 lines)
Code
Loading syntax highlighter...

Pattern 3: Golden Master Testing

JAVA(48 lines)
Code
Loading syntax highlighter...

Pattern 4: Testing Without Seams

JAVA(81 lines)
Code
Loading syntax highlighter...

Pattern 5: Capturing Side Effects

JAVA(51 lines)
Code
Loading syntax highlighter...

Pattern 6: Database-Dependent Tests

JAVA(59 lines)
Code
Loading syntax highlighter...

Pattern 7: Documenting Discovered Behavior

JAVA(69 lines)
Code
Loading syntax highlighter...

⚠️ Common Mistakes

Mistake 1: Fixing Bugs in Characterization Tests

JAVA(14 lines)
Code
Loading syntax highlighter...

Mistake 2: Over-Specifying Tests

JAVA(16 lines)
Code
Loading syntax highlighter...

🐛 Debug This: The False Safety Net

A developer wrote characterization tests before refactoring. The tests passed before and after the refactoring. But production crashed with wrong calculations. "I had tests! How did this get through?"

JAVA(83 lines)
Code
Loading syntax highlighter...
The refactoring broke production but tests passed. What's wrong with these tests?

✅ Solution:

These aren't characterization tests - they're just regular mocked unit tests!

Problems:
  1. Mocks don't capture real behavior
    JAVA(2 lines)
    Code
    Loading syntax highlighter...

    The mock doesn't return real tax rates. Characterization tests should use real data or captured production data.

  2. Expected values are guessed, not discovered
    JAVA
    Code
    Loading syntax highlighter...

    The developer calculated 100 * 0.0725 = 7.25 mentally. But what if the real code does something different (like adding fees)?

  3. Weak assertion on complex logic
    JAVA
    Code
    Loading syntax highlighter...

    The exemption test only checks for non-null. Actual exemption logic isn't characterized at all.

  4. Missing edge cases from production
    • What happens with zero subtotal?
    • What about negative amounts (returns)?
    • Multiple exempt items?
    • Tax rate of 0%?
Correct characterization tests:
JAVA(46 lines)
Code
Loading syntax highlighter...
The lesson: Characterization tests must capture ACTUAL behavior, not expected behavior. Use real data, not mocks. Discover values by running tests, don't calculate them.

💻 Exercises

Exercise 1: Write Basic Characterization Test

⭐ Difficulty: Easy | ⏱️ Time: 10 minutes

Task: Write a characterization test for this legacy code to discover its actual behavior.
JAVA(14 lines)
Code
Loading syntax highlighter...
Write tests to discover: formatting for all locales, negative numbers, zero, large numbers, unknown locale.
✅ Solution:
JAVA(66 lines)
Code
Loading syntax highlighter...

Exercise 2: Golden Master Test

⭐⭐ Difficulty: Medium | ⏱️ Time: 20 minutes

Task: Create a golden master test for this report generator.
JAVA(23 lines)
Code
Loading syntax highlighter...
✅ Solution:
JAVA(102 lines)
Code
Loading syntax highlighter...

Exercise 3: Capture Side Effects

⭐⭐ Difficulty: Medium | ⏱️ Time: 15 minutes

Task: Write characterization tests that capture all side effects of this order processor.
JAVA(22 lines)
Code
Loading syntax highlighter...
Document all side effects: what is saved, reserved, sent, and recorded.
✅ Solution:
JAVA(120 lines)
Code
Loading syntax highlighter...

Exercise 4: Parameterized Characterization

⭐⭐⭐ Difficulty: Medium-Hard | ⏱️ Time: 20 minutes

Task: Create comprehensive parameterized tests to fully characterize this discount calculator's behavior.
JAVA(29 lines)
Code
Loading syntax highlighter...
Cover: all tiers, volume thresholds, loyalty bonus, discount cap.
✅ Solution:
JAVA(133 lines)
Code
Loading syntax highlighter...

Exercise 5: Database Characterization Test

⭐⭐⭐⭐ Difficulty: Hard | ⏱️ Time: 25 minutes

Task: Write characterization tests for this query-heavy service using TestContainers.
JAVA(32 lines)
Code
Loading syntax highlighter...
Test with real database: verify query behavior, edge cases, ordering, data types.
✅ Solution:
JAVA(180 lines)
Code
Loading syntax highlighter...

📝 Summary

TechniqueWhen to Use
Basic CharacterizationSingle method behavior
Parameterized TestsExploring input/output matrix
Golden MasterComplex outputs (reports, JSON)
Subclass and OverrideStatic dependencies
Side Effect CaptureDatabase writes, emails, etc.

📅 Review Schedule for This Article

DayTaskTime
Day 1Review difference between characterization tests and unit tests5 min
Day 3Redo Exercise 1 (Basic Characterization Test)10 min
Day 7Practice golden master testing on a report in your codebase15 min
Day 14Redo Debug This (The False Safety Net)10 min
Day 30Write characterization tests for a legacy feature you'll modify25 min

Next: [Part 21: Breaking Dependencies]