Test Doubles

Learning Objectives

Distinguish dummy, stub, fake, spy and mock.

Choose the right double and avoid over-mocking.

Write contract tests that run against both real implementations and fakes.

Prerequisites: Modules 22, 64.

Intuition and Definition

Fills a parameter; never used nullptr logger that is never called Returns canned answers clock that always says 09:00 Working, simplified implementation

Records how it was called, for later assertions notifier that stores sent messages

Pre-programmed expectations, verifies interactions "expect charge(42.0) exactly once"

Why It Matters

Doubles isolate the unit under test from slow, non-deterministic or unavailable collaborators (databases, networks, hardware, time) — but every double is an assumption about the real thing.

Syntax and Language Notes

from unittest.mock import Mock
gateway = Mock()
gateway.charge.return_value = True
service.checkout(gateway, 42)
gateway.charge.assert_called_once_with(42)

Complete Example

A contract test — the same test suite runs against the fake and the "real" implementation, so the fake can't drift from the real behaviour:

#include <functional>
#include <iostream>
#include <map>
#include <memory>
#include <optional>
#include <string>

struct Repository {                                       // the contract
    virtual ~Repository() = default;
    virtual void save(const std::string& vin, int km) = 0;
    virtual std::optional<int> find(const std::string& vin) const = 0;
};

struct InMemoryRepository : Repository {                  // FAKE used by unit tests
    std::map<std::string, int> data;
    void save(const std::string& v, int km) override { data[v] = km; }
    std::optional<int> find(const std::string& v) const override { auto it = data.find(v); if (it == data.end()) return std::nullopt; return it->second; }
};

struct FileRepository : Repository {                      // stand-in for the real database adapter
    std::map<std::string, std::string> rows;              // pretend: rows as text
    void save(const std::string& v, int km) override { rows[v] = std::to_string(km); }
    std::optional<int> find(const std::string& v) const override { auto it = rows.find(v); if (it == rows.end()) return std::nullopt; return std::stoi(it->second); }
};

// contract tests: every implementation must pass
…
InMemoryRepository: passes the repository contract
FileRepository: passes the repository contract
spy recorded 1 message: V1 due for service

Step-by-Step Walkthrough

contract() encodes the behaviour every Repository must have; it runs unchanged against both implementations.

Unit tests of services use the in-memory fake (fast, deterministic) with confidence that it behaves like the real adapter.

The spy records interactions; the test asserts on what was sent, not on internal calls.

Common Mistakes

Over-mocking: mocking every collaborator and verifying every call — tests mirror the implementation and break on harmless refactors.

Mocking types you don't own (third-party SDKs) — wrap them in your own adapter interface and mock that.

Mocking value objects — just create real ones.

Fakes that drift from the real implementation — use contract tests.

Trade-offs and Alternatives

Prefer real objects for values and pure logic, fakes for infrastructure (repositories, clocks, file systems), spies for outgoing notifications, and mocks sparingly for interactions that *are* the behaviour (e.g. "the payment must be charged exactly once").

Real-World Use

In-memory databases/repositories in unit tests, fake clocks, WireMock/MSW for HTTP, hardware simulators for drivers, contract testing between microservices (Pact).

Practice

Q1. (Easy) Which double has a working, simplified implementation?

Answer: A fake.

Q2. (Medium) Spy vs mock?

Answer: A spy records calls for assertions after the act; a mock is configured with expectations beforehand and verifies them (often failing immediately on unexpected calls).

Q3. (Advanced) How do contract tests prevent fake drift?

Answer: The same behavioural test suite runs against the fake and the real implementation (in integration test runs), so any difference in behaviour fails a test.

Interview Question

Q1. "When are mocks harmful?" Answer: When they replace things that are cheap and deterministic anyway (values, pure functions), when tests verify incidental interactions (call order, exact arguments that don't matter), and when they mock third-party types whose real behaviour differs. The result is brittle tests that pass while production fails. Use real objects where possible, fakes for infrastructure with contract tests, and mocks only for interactions that are the essential outcome.

Quick Quiz

Q1. Which double returns canned answers?

Answer: A stub.

Q2. Which double should replace a database in fast unit tests?

Answer: A fake (in-memory implementation).

Q3. Should you mock third-party APIs directly?

Answer: Generally no — wrap them in your own interface and mock that.

Summary

Dummy, stub, fake, spy, mock — choose by need; don't over-mock.

Contract tests keep fakes honest.

Related: Modules 22, 64, 66.