Testing Without Mocks
Apply James Shore's pattern language for writing tests that are narrow, fast, deterministic, and mock-free.
Full reference: https://www.jamesshore.com/v2/projects/nullables/testing-without-mocks
Goals
Write tests that satisfy ALL of these properties:
- No broad tests required — only narrow tests focused on specific behaviors; smoke tests are a safety net, not the strategy
- Easy refactoring — test behavior/outcomes, not interactions or method calls between objects
- Readable tests — straightforward arrange/act/assert; tests document externally-visible behavior
- No magic — no DI frameworks, auto-mocking, or reflection tricks required
- Fast and deterministic — entire suite runs in 1–2 seconds with no flaky failures
Core Decision: Which Pattern Category?
Classify the code under test, then apply the right pattern group:
- Pure logic / values → Logic Patterns (easiest to test; prefer this)
- External system interaction → Infrastructure Patterns (wrap, then make Nullable)
- Orchestration of logic + infra → Architectural Patterns (Logic Sandwich / Traffic Cop with Nullables)
- Legacy code with mocks → Legacy Patterns (incremental migration)
Pattern Summary
For detailed pattern descriptions and examples, see references/patterns.md.
Foundational Patterns
- Narrow Tests — test one concept per test, not the whole system
- State-Based Tests — assert on outputs/state, never on how dependencies were called
- Overlapping Sociable Tests — let tests exercise real dependencies (not mocked); overlapping coverage across units catches integration gaps
- Smoke Tests — one or two end-to-end tests as a safety net only
- Zero-Impact Instantiation — constructors must do no I/O, no connections, no heavy work
- Parameterless Instantiation — provide factory methods with sensible defaults so tests can create objects without specifying every parameter
- Signature Shielding — use options/config objects so adding parameters does not break existing tests
Architectural Patterns
- A-Frame Architecture — structure code so Infrastructure and Logic are peers under an Application layer, with no direct dependencies between them
- Logic Sandwich — Application method reads infra → computes with logic → writes infra (three steps, no interleaving)
- Traffic Cop — for cases where logic and infra must interleave, use an event-based or callback approach; keep the traffic cop thin and test it with Nullables
- Grow Evolutionary Seeds — start with a walking skeleton, grow architecture incrementally
Logic Patterns
- Easily-Visible Behavior — prefer pure functions and immutable objects; for mutable objects, expose state via getters or events
- Testable Libraries — wrap third-party libraries that are hard to test into your own API with visible behavior
- Collaborator-Based Isolation — when one logical unit depends on another, test each unit focused on its own behavior; don't mock the collaborator, let sociable tests run through it
Infrastructure Patterns
- Infrastructure Wrappers — one wrapper class per external system; clean internal API, messy external details hidden
- Narrow Integration Tests — test wrappers against the real external system; focused, slow-but-necessary tests for infra only
- Paranoic Telemetry — monitor production to catch integration issues that tests cannot
Nullability Patterns
- Nullables — production classes get a
createNull() factory that returns an instance with infrastructure disabled but all logic intact
- Embedded Stub — the Nullable's stub logic lives inside the production class (not in a separate test file), controlled by a flag
- Thin Wrapper — when third-party code is hard to stub, create a thin wrapper whose sole job is to call the third party, then embed the stub in your wrapper
- Configurable Responses —
createNull() accepts optional parameters to control what the Nulled instance returns, defined in terms of the wrapper's public API, not its implementation
- Output Tracking — Nulled instances can record what would have been sent to the external system, enabling state-based assertions on side effects
- Behavior Simulation — for complex infrastructure (e.g., stateful APIs), the embedded stub simulates realistic behavior
- Fake It Once You Make It — when converting legacy code, start by making direct dependencies Nullable and work down the tree
Legacy Code Patterns
- Descend the Ladder — convert one module and its direct deps at a time, working down the dependency tree
- Climb the Ladder — alternatively, start from the lowest infrastructure and work up
- Replace Mocks with Nullables — swap mocks for Nullables one dependency at a time
- Throwaway Stub — create a temporary test double to fill in while you convert deeper dependencies; remove it when you reach that layer
Workflow: Writing a New Test
- Classify the code: pure logic, infrastructure wrapper, or orchestration?
- If pure logic → write state-based tests directly with real collaborators
- If infrastructure wrapper → write narrow integration tests against the real system, then add
createNull() with Embedded Stub
- If orchestration → ensure dependencies are Nullable, then write tests using
createNull() with Configurable Responses and Output Tracking
- Verify: does the test assert on outputs/state only (not on calls to dependencies)?
- Verify: does the test use arrange/act/assert structure?
- Verify: would a structural refactoring break this test? If yes, revise.
Workflow: Removing Mocks from Existing Tests
- Identify the mock/spy being used
- Determine what the mock's target is: infrastructure or logic?
- If logic → remove the mock; use real collaborator (sociable test)
- If infrastructure → make the infrastructure class Nullable, use
createNull() with Configurable Responses and Output Tracking
- Delete the mock setup; replace with
createNull()-based instantiation
- Assert on outputs/state instead of verifying mock interactions
1---2name: testing-without-mocks3description: Apply James Shore's "Testing Without Mocks" pattern language to write tests that are narrow, sociable, state-based, and free of mocks/spies. Use this skill when writing or refactoring tests for any codebase, when the user asks to test code without mocks, when applying Nullable infrastructure patterns, when structuring code with A-Frame Architecture for testability, or when converting legacy mock-heavy tests. Triggers: "test without mocks", "nullable pattern", "sociable tests", "A-Frame architecture", "remove mocks", "testing-without-mocks", "narrow tests", "infrastructure wrapper", "embedded stub".4---56# Testing Without Mocks78Apply James Shore's pattern language for writing tests that are narrow, fast, deterministic, and mock-free.9Full reference: https://www.jamesshore.com/v2/projects/nullables/testing-without-mocks1011## Goals1213Write tests that satisfy ALL of these properties:14151. **No broad tests required** — only narrow tests focused on specific behaviors; smoke tests are a safety net, not the strategy162. **Easy refactoring** — test behavior/outcomes, not interactions or method calls between objects173. **Readable tests** — straightforward arrange/act/assert; tests document externally-visible behavior184. **No magic** — no DI frameworks, auto-mocking, or reflection tricks required195. **Fast and deterministic** — entire suite runs in 1–2 seconds with no flaky failures2021## Core Decision: Which Pattern Category?2223Classify the code under test, then apply the right pattern group:2425- **Pure logic / values** → Logic Patterns (easiest to test; prefer this)26- **External system interaction** → Infrastructure Patterns (wrap, then make Nullable)27- **Orchestration of logic + infra** → Architectural Patterns (Logic Sandwich / Traffic Cop with Nullables)28- **Legacy code with mocks** → Legacy Patterns (incremental migration)2930## Pattern Summary3132For detailed pattern descriptions and examples, see [references/patterns.md](references/patterns.md).3334### Foundational Patterns3536- **Narrow Tests** — test one concept per test, not the whole system37- **State-Based Tests** — assert on outputs/state, never on how dependencies were called38- **Overlapping Sociable Tests** — let tests exercise real dependencies (not mocked); overlapping coverage across units catches integration gaps39- **Smoke Tests** — one or two end-to-end tests as a safety net only40- **Zero-Impact Instantiation** — constructors must do no I/O, no connections, no heavy work41- **Parameterless Instantiation** — provide factory methods with sensible defaults so tests can create objects without specifying every parameter42- **Signature Shielding** — use options/config objects so adding parameters does not break existing tests4344### Architectural Patterns4546- **A-Frame Architecture** — structure code so Infrastructure and Logic are peers under an Application layer, with no direct dependencies between them47- **Logic Sandwich** — Application method reads infra → computes with logic → writes infra (three steps, no interleaving)48- **Traffic Cop** — for cases where logic and infra must interleave, use an event-based or callback approach; keep the traffic cop thin and test it with Nullables49- **Grow Evolutionary Seeds** — start with a walking skeleton, grow architecture incrementally5051### Logic Patterns5253- **Easily-Visible Behavior** — prefer pure functions and immutable objects; for mutable objects, expose state via getters or events54- **Testable Libraries** — wrap third-party libraries that are hard to test into your own API with visible behavior55- **Collaborator-Based Isolation** — when one logical unit depends on another, test each unit focused on its own behavior; don't mock the collaborator, let sociable tests run through it5657### Infrastructure Patterns5859- **Infrastructure Wrappers** — one wrapper class per external system; clean internal API, messy external details hidden60- **Narrow Integration Tests** — test wrappers against the real external system; focused, slow-but-necessary tests for infra only61- **Paranoic Telemetry** — monitor production to catch integration issues that tests cannot6263### Nullability Patterns6465- **Nullables** — production classes get a `createNull()` factory that returns an instance with infrastructure disabled but all logic intact66- **Embedded Stub** — the Nullable's stub logic lives inside the production class (not in a separate test file), controlled by a flag67- **Thin Wrapper** — when third-party code is hard to stub, create a thin wrapper whose sole job is to call the third party, then embed the stub in your wrapper68- **Configurable Responses** — `createNull()` accepts optional parameters to control what the Nulled instance returns, defined in terms of the wrapper's public API, not its implementation69- **Output Tracking** — Nulled instances can record what would have been sent to the external system, enabling state-based assertions on side effects70- **Behavior Simulation** — for complex infrastructure (e.g., stateful APIs), the embedded stub simulates realistic behavior71- **Fake It Once You Make It** — when converting legacy code, start by making direct dependencies Nullable and work down the tree7273### Legacy Code Patterns7475- **Descend the Ladder** — convert one module and its direct deps at a time, working down the dependency tree76- **Climb the Ladder** — alternatively, start from the lowest infrastructure and work up77- **Replace Mocks with Nullables** — swap mocks for Nullables one dependency at a time78- **Throwaway Stub** — create a temporary test double to fill in while you convert deeper dependencies; remove it when you reach that layer7980## Workflow: Writing a New Test81821. Classify the code: pure logic, infrastructure wrapper, or orchestration?832. If pure logic → write state-based tests directly with real collaborators843. If infrastructure wrapper → write narrow integration tests against the real system, then add `createNull()` with Embedded Stub854. If orchestration → ensure dependencies are Nullable, then write tests using `createNull()` with Configurable Responses and Output Tracking865. Verify: does the test assert on outputs/state only (not on calls to dependencies)?876. Verify: does the test use arrange/act/assert structure?887. Verify: would a structural refactoring break this test? If yes, revise.8990## Workflow: Removing Mocks from Existing Tests91921. Identify the mock/spy being used932. Determine what the mock's target is: infrastructure or logic?943. If logic → remove the mock; use real collaborator (sociable test)954. If infrastructure → make the infrastructure class Nullable, use `createNull()` with Configurable Responses and Output Tracking965. Delete the mock setup; replace with `createNull()`-based instantiation976. Assert on outputs/state instead of verifying mock interactions