Skip to content

Test Doubles

A test double is any object substituted for a real dependency in a test, the way a stunt double stands in for an actor. The term is Gerard Meszaros's, and it exists because "mock" had become a name for all five kinds, which made conversations about testing imprecise.

graph TD
    TD[Test double] --> DU["Dummy: passed but never used"]
    TD --> ST["Stub: returns canned answers"]
    TD --> SP["Spy: a stub that records how it was called"]
    TD --> MO["Mock: pre-programmed with expectations, asserts on them"]
    TD --> FA["Fake: a working but simplified implementation"]
Double Provides behaviour Records calls Asserts on calls Typical use
Dummy no no no Fill a required parameter
Stub yes, canned no no Supply input the test needs
Spy yes yes after the fact Check something was called, without pre-programming
Mock yes yes yes, built in Verify an interaction is the point of the test
Fake yes, real logic no no Replace slow infrastructure

Why substitute anything

graph LR
    U[Unit under test] --> D1[Slow dependency: network, database]
    U --> D2[Non-deterministic: clock, random, environment]
    U --> D3[Hard to trigger: error paths, timeouts]
    U --> D4[Side effects: charges money, sends mail]
    U --> D5[Not built yet]

Those five are the legitimate reasons. Anything fast, deterministic and side-effect free should be used for real, because a real collaborator tests the real integration for free.

State verification and behaviour verification

The distinction that decides which double to use.

State verification Behaviour verification
Asserts on The result or resulting state The calls that were made
Uses Stubs and fakes Mocks and spies
Survives refactoring Yes Often not
Right when The outcome is what matters The interaction is the outcome

Prefer state verification. Reach for behaviour verification when the call itself is the requirement: "the payment provider is charged exactly once", "the audit event is emitted", "no email is sent when the order is a draft". In those cases there is no state to inspect, so the call is the observable effect.

Over-mocking, the standard failure

graph LR
    M[Mock every collaborator] --> C[Test asserts on internal call sequences]
    C --> R[Refactor changes the sequence]
    R --> F[Tests fail although behaviour is unchanged]
    F --> T[Team stops trusting the suite]
    M --> V[Tests pass while the real integration is broken]

Two costs, and the second is worse. A suite of heavily mocked tests can be entirely green while the system does not work, because every assumption encoded in the mocks was wrong in the same way the code was.

Symptoms worth acting on: a test with more setup than assertion, a mock returning a mock, or a test that must change whenever a private method is renamed.

Choosing quickly

graph TD
    Q1{Is the dependency fast, deterministic and free of side effects?} -->|yes| REAL[Use the real thing]
    Q1 -->|no| Q2{Is it only there to satisfy a signature?}
    Q2 -->|yes| DUM[Dummy]
    Q2 -->|no| Q3{Does the test need it to return something?}
    Q3 -->|yes| Q4{Is the interaction itself the requirement?}
    Q3 -->|no| SPY[Spy, if the call must be observed]
    Q4 -->|no| STUB[Stub, or a Fake for heavy infrastructure]
    Q4 -->|yes| MOCK[Mock]

Check Your Understanding

#

When is behaviour verification with a mock the right choice over state verification?

#

A test suite mocks every collaborator, is entirely green, and the deployed system is broken. What happened?