d9e5668cec
Fixes and improvements from exhaustive audit: Consistency fixes in SKILL.md: - 'Pipe & Filter' → 'Pipe and Filter' (one stray '&' found and corrected) - 'Singleton for factory instance' → clarified to 'register factory as singleton-scoped via DI container' (less misleading wording) - Documentation Format section updated with note that SKILL.md itself is the authoritative source for related-pattern combinations Coverage fix — Related Patterns sections: - Added '## Related Patterns' to ALL 94 pattern files (was 0/94) - Each section lists 3–6 related patterns with relationship descriptions - Covers: why they're related, when to prefer one vs the other, and which are often confused SOLID principles → Creational → Structural → Behavioral → Architectural → Concurrency → Functional → Resilience → Data Access → Messaging → Testing → Error Handling → Microservice — all 13 categories covered Code verification: - Python: 0 failures (all 85 testable blocks pass) - Go: 0 failures (all 76 testable blocks pass) - JavaScript: 0 failures (all 78 testable blocks pass) - All 239 code blocks verified correct after edits Final skill state: - 108 files, 36,524 lines across 13 reference categories - 94/94 pattern files have Related Patterns sections - 2,815-line SKILL.md with 67 decision trees, 23 scenarios, 0 broken references, 0 naming inconsistencies
12 KiB
12 KiB
Test Doubles
Problem
A unit under test depends on external components (databases, APIs, file systems, other services). Running real dependencies makes tests slow, flaky, and hard to control. You need a way to replace those dependencies with controlled substitutes so you can test behaviour in isolation.
Solution
Use test doubles — objects that stand in for real dependencies during testing. There are five kinds:
| Double | Purpose | Behaviour |
|---|---|---|
| Dummy | Fill a required parameter slot | Does nothing; never actually called |
| Stub | Return canned answers | Provides predetermined responses |
| Spy | Record interactions for later assertion | Wraps real or stub behaviour, logs calls |
| Mock | Verify expected interactions | Pre-programmed with expectations; fails if not met |
| Fake | Lightweight working implementation | Works but takes shortcuts (e.g., in-memory DB) |
When to Use
- Unit testing a class/function that depends on I/O or external services.
- You need deterministic, fast tests that don't depend on network or disk.
- You want to verify that your code interacts correctly with its collaborators.
When to Avoid
- The real dependency is fast, deterministic, and side-effect-free (e.g., a pure utility).
- Over-mocking leads to tests that pass even when the real integration is broken.
- Integration or end-to-end tests where you intentionally want real components.
Pseudocode
// Stub example
stub_payment_gateway = create_stub(PaymentGateway)
stub_payment_gateway.when_called("charge").return(Success(transaction_id="tx_123"))
order_service = OrderService(payment=stub_payment_gateway)
result = order_service.place_order(item="Widget", amount=9.99)
assert result.is_success
assert result.transaction_id == "tx_123"
// Spy example
spy_logger = create_spy(Logger)
user_service = UserService(logger=spy_logger)
user_service.create_user("alice")
assert spy_logger.was_called_with("log", "User created: alice")
// Fake example
fake_db = InMemoryDatabase()
repo = UserRepository(db=fake_db)
repo.save(User("alice"))
assert repo.find_by_name("alice") is not None
Python
"""Test Doubles — Mock, Stub, Spy, Fake, Dummy in Python."""
from unittest.mock import MagicMock, patch, call
from dataclasses import dataclass, field
from typing import Any
# ── Production code ────────────────────────────────────────────
@dataclass
class User:
name: str
email: str
class EmailService:
def send(self, to: str, subject: str, body: str) -> bool:
raise RuntimeError("Real email service — should not be called in tests")
class UserRepository:
def save(self, user: User) -> None:
raise RuntimeError("Real DB — should not be called in tests")
def find_by_name(self, name: str) -> User | None:
raise RuntimeError("Real DB — should not be called in tests")
class UserService:
def __init__(self, repo: UserRepository, email: EmailService, logger: Any = None):
self.repo = repo
self.email = email
self.logger = logger
def register(self, name: str, email_addr: str) -> User:
user = User(name=name, email=email_addr)
self.repo.save(user)
self.email.send(to=email_addr, subject="Welcome", body=f"Hi {name}")
if self.logger:
self.logger.log(f"Registered {name}")
return user
# ── Fake implementation ────────────────────────────────────────
class FakeUserRepository(UserRepository):
"""In-memory fake — works but takes a shortcut (no real DB)."""
def __init__(self):
self._store: dict[str, User] = {}
def save(self, user: User) -> None:
self._store[user.name] = user
def find_by_name(self, name: str) -> User | None:
return self._store.get(name)
# ── Spy implementation ─────────────────────────────────────────
class SpyLogger:
"""Records every call for later assertion."""
def __init__(self):
self.calls: list[str] = []
def log(self, message: str) -> None:
self.calls.append(message)
# ── Tests ──────────────────────────────────────────────────────
def test_dummy():
"""Dummy: passed but never used."""
dummy_logger = None # UserService accepts None — never calls it if None
fake_repo = FakeUserRepository()
stub_email = MagicMock(spec=EmailService)
stub_email.send.return_value = True
svc = UserService(repo=fake_repo, email=stub_email, logger=dummy_logger)
user = svc.register("alice", "a@test.com")
print(f"[Dummy] Registered: {user.name}")
def test_stub():
"""Stub: returns canned answers."""
stub_email = MagicMock(spec=EmailService)
stub_email.send.return_value = True # canned response
fake_repo = FakeUserRepository()
svc = UserService(repo=fake_repo, email=stub_email)
user = svc.register("bob", "b@test.com")
assert user.name == "bob"
print(f"[Stub] Email stub returned: {stub_email.send.return_value}")
def test_spy():
"""Spy: records interactions for later assertion."""
spy_logger = SpyLogger()
fake_repo = FakeUserRepository()
stub_email = MagicMock(spec=EmailService)
svc = UserService(repo=fake_repo, email=stub_email, logger=spy_logger)
svc.register("carol", "c@test.com")
assert spy_logger.calls == ["Registered carol"]
print(f"[Spy] Logger recorded: {spy_logger.calls}")
def test_mock():
"""Mock: pre-programmed expectations, verified after."""
mock_email = MagicMock(spec=EmailService)
fake_repo = FakeUserRepository()
svc = UserService(repo=fake_repo, email=mock_email)
svc.register("dave", "d@test.com")
mock_email.send.assert_called_once_with(
to="d@test.com", subject="Welcome", body="Hi dave"
)
print(f"[Mock] Email mock verified call: {mock_email.send.call_args}")
def test_fake():
"""Fake: lightweight working implementation."""
fake_repo = FakeUserRepository()
stub_email = MagicMock(spec=EmailService)
svc = UserService(repo=fake_repo, email=stub_email)
svc.register("eve", "e@test.com")
found = fake_repo.find_by_name("eve")
assert found is not None and found.email == "e@test.com"
print(f"[Fake] Found in fake repo: {found}")
if __name__ == "__main__":
test_dummy()
test_stub()
test_spy()
test_mock()
test_fake()
print("\nAll test-double demonstrations passed.")
JavaScript
// test_doubles.js — Mock, Stub, Spy, Fake, Dummy in JavaScript
// ── Production code ──────────────────────────────────────────
class EmailService {
send(to, subject, body) {
throw new Error("Real email service — should not be called in tests");
}
}
class UserRepository {
save(user) {
throw new Error("Real DB — should not be called in tests");
}
findByName(name) {
throw new Error("Real DB — should not be called in tests");
}
}
class UserService {
constructor(repo, email, logger = null) {
this.repo = repo;
this.email = email;
this.logger = logger;
}
register(name, emailAddr) {
const user = { name, email: emailAddr };
this.repo.save(user);
this.email.send(emailAddr, "Welcome", `Hi ${name}`);
if (this.logger) this.logger.log(`Registered ${name}`);
return user;
}
}
// ── Fake ─────────────────────────────────────────────────────
class FakeUserRepository {
constructor() {
this._store = new Map();
}
save(user) {
this._store.set(user.name, user);
}
findByName(name) {
return this._store.get(name) || null;
}
}
// ── Spy ──────────────────────────────────────────────────────
class SpyLogger {
constructor() {
this.calls = [];
}
log(message) {
this.calls.push(message);
}
}
// ── Stub helper ──────────────────────────────────────────────
function createStubEmail() {
return { send: () => true, _calls: [] };
}
// ── Mock helper ──────────────────────────────────────────────
function createMockEmail() {
const mock = {
_calls: [],
send(to, subject, body) {
mock._calls.push({ to, subject, body });
return true;
},
assertCalledWith(expected) {
const actual = mock._calls[0];
console.assert(
actual.to === expected.to &&
actual.subject === expected.subject &&
actual.body === expected.body,
"Mock assertion failed"
);
},
};
return mock;
}
// ── Tests ────────────────────────────────────────────────────
function testDummy() {
const dummyLogger = null; // passed but never invoked
const fakeRepo = new FakeUserRepository();
const stubEmail = createStubEmail();
const svc = new UserService(fakeRepo, stubEmail, dummyLogger);
const user = svc.register("alice", "a@test.com");
console.log(`[Dummy] Registered: ${user.name}`);
}
function testStub() {
const stubEmail = createStubEmail();
const fakeRepo = new FakeUserRepository();
const svc = new UserService(fakeRepo, stubEmail);
const user = svc.register("bob", "b@test.com");
console.assert(user.name === "bob");
console.log(`[Stub] Email stub returned: true`);
}
function testSpy() {
const spyLogger = new SpyLogger();
const fakeRepo = new FakeUserRepository();
const stubEmail = createStubEmail();
const svc = new UserService(fakeRepo, stubEmail, spyLogger);
svc.register("carol", "c@test.com");
console.assert(spyLogger.calls[0] === "Registered carol");
console.log(`[Spy] Logger recorded: ${JSON.stringify(spyLogger.calls)}`);
}
function testMock() {
const mockEmail = createMockEmail();
const fakeRepo = new FakeUserRepository();
const svc = new UserService(fakeRepo, mockEmail);
svc.register("dave", "d@test.com");
mockEmail.assertCalledWith({
to: "d@test.com",
subject: "Welcome",
body: "Hi dave",
});
console.log(
`[Mock] Email mock verified call: ${JSON.stringify(mockEmail._calls[0])}`
);
}
function testFake() {
const fakeRepo = new FakeUserRepository();
const stubEmail = createStubEmail();
const svc = new UserService(fakeRepo, stubEmail);
svc.register("eve", "e@test.com");
const found = fakeRepo.findByName("eve");
console.assert(found !== null && found.email === "e@test.com");
console.log(`[Fake] Found in fake repo: ${JSON.stringify(found)}`);
}
testDummy();
testStub();
testSpy();
testMock();
testFake();
console.log("\nAll test-double demonstrations passed.");
Related Patterns
- Dependency Injection — DI is the enabler; it makes dependencies replaceable with test doubles at construction time.
- Builder — use a Test Data Builder to construct complex test doubles or fixture objects with sensible defaults.
- Null Object — a Null Object is a production-safe test double: it satisfies the interface without any behaviour.