Test Observable Behavior
test-observable-behavior
Summary
Tests that pin private structure can block useful refactors while missing broken user outcomes. Test observable outputs, errors, side effects, state, events, and boundaries, reserving private tests for dense rules that are otherwise hard to reach.
Problem
Tests that pin private helper calls, incidental structure, or exact internal sequencing make refactors harder without protecting the behavior callers depend on. A change can pass those tests while still breaking the public result.
Preferred Move
Write tests around behavior that callers can observe: outputs, errors, side effects, persisted state, emitted events, or integration boundaries. Name the behavior under test rather than the private implementation path.
Tradeoff
Some private logic deserves direct tests when it contains dense rules or hard-to-reach edge cases. Keep those tests focused on the rule they protect, and avoid making them a contract for incidental helper shape.
Agent Instruction
Prefer behavior-level tests for changed paths. Do not add tests that freeze private implementation
shape unless that private rule is the real risk being protected.Examples
Bad: the test freezes the helper chosen by the current implementation.
assert_eq!(parse_rows_called(), 1);
Good: the test protects the output callers depend on.
assert_eq!(parse_report(input)?.rows.len(), 3);
References
| Source | Use | Note |
|---|---|---|
| Feathers | adapts |
Legacy-code work depends on tests that preserve behavior. |
| Refactoring | adapts |
Refactoring relies on preserving externally visible behavior. |