| name | testing-strategy |
| description | Testing strategy and design decisions — what to test, how to test, when to mock, test classification, TDD rhythm. Use when: deciding test approach, choosing test scope, reviewing test quality, legacy code testing, mock vs real debate. Triggers: testing strategy, what to test, test design, mock, TDD, test pyramid, test doubles, legacy testing, characterization test |
Testing Strategy
Test design judgment framework. Not code design (→ design-philosophy), not production monitoring (→ production-resilience).
Classical (Detroit) vs London School
| Dimension | Classical (Detroit) | London (Mockist) |
|---|
| Unit | Unit of behavior | Single class |
| Collaborators | Real objects | All mocked |
| Mocks | Only shared deps (DB, filesystem, external API) | All dependencies |
| Refactoring resistance | High — tests survive internal restructuring | Low — tests coupled to implementation |
| Fault isolation | Lower — failure may propagate | High — pinpoints broken class |
| Recommendation | Prefer for most cases | Use selectively for complex interaction verification |
Four Pillars of a Good Test (Khorikov)
| Pillar | Optimizing only this → |
|---|
| Protection against regressions | Slow, broad tests |
| Resistance to refactoring | Tests that never fail (useless) |
| Fast feedback | Shallow tests missing real bugs |
| Maintainability | Over-abstracted test infrastructure |
You cannot maximize all four. Acknowledge trade-offs explicitly. Tests optimizing a single pillar tend to be fragile or useless.
Test Classification by Collaborator Type
| Code Type | Test Type | Mock? |
|---|
| Domain model / algorithms | Unit test | No — high value, pure logic |
| Controllers / orchestrators | Integration test | Minimal — verify wiring |
| External dependencies (DB, API, filesystem) | Mock at boundary | Yes — use test doubles |
| Trivial code (getters, simple delegation) | Don't test | N/A |
Testing Styles — Prefer Output-Based
| Style | Verifies | Coupling | Preference |
|---|
| Output-based | Return value of function | Lowest | Prefer — functional core |
| State-based | Object state after action | Medium | Acceptable |
| Communication-based | Calls to collaborators (mocks) | Highest | Use sparingly |
Humble Object Pattern
- Extract logic from hard-to-test code (UI, infrastructure) into testable pure functions
- Hard-to-test shell becomes thin and trivial — the "humble" part
- Test the extracted logic thoroughly; skip or integration-test the shell
TDD Rhythm (Beck)
- Red — write the simplest failing test
- Green — make it pass with minimum code
- Refactor — clean up while tests stay green
- Triangulation: add tests to force generalization from specific to general
- When NOT to TDD: exploratory spikes, trivial code, throwaway prototypes
Test Double Taxonomy (Meszaros)
| Double | Purpose | Behavior |
|---|
| Dummy | Fill parameter lists | Never actually used |
| Stub | Provide canned answers | Returns pre-configured data |
| Spy | Record interactions | Stub + captures calls for later assertion |
| Mock | Verify interactions | Pre-programmed expectations, fails if not met |
| Fake | Working implementation | Lightweight substitute (e.g., in-memory DB) |
Use correct terms — "mock" is not a synonym for all test doubles.
Test Smells (Meszaros)
| Smell | Signal |
|---|
| Fragile Test | Breaks on unrelated changes — coupled to implementation |
| Obscure Test | Hard to understand intent — too much setup or indirection |
| Slow Test | Discourages frequent runs — move logic to unit-testable core |
| Erratic Test | Passes/fails nondeterministically — shared state or timing |
AAA Pattern
- Arrange — set up preconditions and inputs
- Act — execute the behavior under test
- Assert — verify expected outcome
- One logical assertion per test (guideline, not dogma)
- Separate the three phases visually with blank lines
Test Pyramid vs Test Trophy
| Shape | Emphasis | Best fit |
|---|
| Pyramid (Cohn) | Many unit → some integration → few E2E | Monoliths, domain-heavy systems |
| Trophy (Dodds) | Focus on integration, fewer unit/E2E | Microservices, IO-heavy systems |
Choice depends on where bugs actually hide in your architecture.
What NOT to Test
- Trivial code with no logic (simple getters, pass-through)
- Framework internals — trust the framework's own tests
- Third-party library behavior — test your usage, not their code
- Private methods — test through public API
Legacy Code Strategy (Feathers)
- Legacy code = code without tests
- Seam — a point where behavior can be altered without editing the code (object seam, preprocessor seam, link seam)
- Characterization test — test existing behavior before changing it. "What does the code actually do?" not "what should it do?"
- Sprout Method / Sprout Class — add new tested code alongside untested code; don't expand the untested surface
- Break dependencies incrementally — don't attempt big-bang test coverage
Decision Checklist
| Question | Action |
|---|
| Pure logic with clear inputs/outputs? | Output-based unit test |
| Orchestration / wiring between components? | Integration test |
| External system interaction? | Mock at boundary |
| Existing untested code needs changes? | Characterization test first |
| Hard-to-test class with embedded logic? | Humble Object → extract and unit test |
| New feature on legacy code? | Sprout Method/Class |
| Exploring unknowns? | Spike first, then TDD |
Reference Books
- Unit Testing: Principles, Practices, and Patterns — Vladimir Khorikov
- Test-Driven Development by Example — Kent Beck
- xUnit Test Patterns: Refactoring Test Code — Gerard Meszaros
- Working Effectively with Legacy Code — Michael Feathers