Files
freemo d9e5668cec fix(skills): comprehensive final audit pass for programming-patterns skill
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
2026-04-15 13:22:13 -04:00

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.");
  • 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.