# 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

```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

```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.
