Stubs¶
A stub is a test double that returns canned answers to the calls made during a test. It exists to supply the input the unit needs, so the test can assert on what the unit did with it.
graph LR
T[Test] -->|configure canned answers| S[Stub]
T --> U[Unit under test]
U -->|query| S
S -->|canned value| U
U --> R[Result]
R --> A[Assertion is here, on the outcome]
The assertion sits on the result, never on the stub. That is what separates a stub from a mock.
What stubs are for¶
| Need | Example |
|---|---|
| Data the unit depends on | An exchange rate provider returning a fixed rate |
| A deterministic clock | now() fixed to a specific timestamp |
| A specific error path | A repository raising a timeout on demand |
| A state hard to reach for real | An account already flagged as delinquent |
| A dependency not built yet | A stub of the pricing service, replaced later |
The error path row is the underrated one. Failure handling is the least tested code in most systems precisely because triggering the failure for real is awkward, and a stub makes it a one-line arrangement.
def test_uses_cached_rate_when_provider_times_out():
provider = StubRateProvider(raises=Timeout)
converter = Converter(provider, cache={"EUR": 1.09})
assert converter.to_eur(100) == 109
Stub the state, then assert on behaviour¶
The pattern that keeps tests robust: use stubs to put the world in the state the scenario describes, then assert only on the outcome.
graph TD
ARR["Arrange: stubs set up the world"] --> ACT[Act: one call]
ACT --> ASS["Assert: outcome or state, not the stub"]
Because nothing is asserted about the stub, the test says nothing about how the unit obtained its data. If the unit is refactored to fetch the rate differently while producing the same result, the test still passes, which is exactly the behaviour wanted from a regression suite.
Stub, fake and mock¶
| Stub | Fake | Mock | |
|---|---|---|---|
| Logic inside | None, canned answers | Real but simplified | None, expectations |
| Handles unforeseen calls | Returns defaults or fails | Behaves correctly | Usually fails |
| Good for | One or two specific answers | A whole subsystem such as a repository | Verifying an outgoing command |
The switch point is worth knowing: once a stub needs several coordinated canned answers and starts tracking what it returned earlier, it is turning into a fake. Write the fake instead, once, and share it across tests.
Pitfalls¶
- Stubbing the unit under test. Partially stubbing the object being tested means the test verifies the stub, not the code.
- Over-specified stubs. A stub configured to respond only to one exact argument list becomes a hidden interaction assertion, and it fails on harmless changes.
- Stale stubs. A stub whose real counterpart has changed shape keeps returning the old format, so the suite is green while production is broken. Contract tests or a shared fake built from the real interface prevent this.
- Stubbing everything. If nearly every collaborator needs a stub, the unit has too many dependencies, and the test is reporting a design problem.
Check Your Understanding¶
What distinguishes a stub from a mock?
A stub has grown to track earlier calls and return different values depending on them. What should happen?