Practical Coding Rounds/Testing Under Time Pressure

Test Doubles: Choosing Stubs, Mocks, and Fakes Fast

The vocabulary interviewers actually use — dummy, stub, spy, mock, fake — and a fast decision rule for which one fits a given dependency, instead of reaching for "mock everything" by default.

!!3/5Theory: 20m

Why "just mock it" is the wrong default

Under time pressure, it's tempting to reach for whatever your framework's mocking library makes easiest and call it done. That works often enough not to be immediately punished, but it misses what this subtopic is actually about: different test doubles answer different questions, and picking the wrong one either weakens the test (a mock that verifies an implementation detail nobody cares about, breaking on every harmless refactor) or makes it slower/flakier than it needs to be (a fake that reimplements a whole subsystem when a one-line stub would do). Knowing the vocabulary and picking deliberately is itself a signal interviewers watch for.

The vocabulary, from Martin Fowler's canonical breakdown

  • Dummy — an object passed around to satisfy a parameter list but never actually used. It exists only because the method signature requires something there.
  • Stub — provides canned answers to calls made during the test, with no real logic behind it. Used when the test cares about the result of code that depends on the stub, not whether or how the stub itself was called.
  • Spy — a stub that also records how it was called, so the test can later assert on that history. A halfway point between a stub and a mock.
  • Mock — pre-programmed with expectations about the calls it should receive, and the test asserts those expectations were met. Used when the test cares about interaction — did the code call this dependency correctly — not (or not only) the return value.
  • Fake — a real, working implementation, just a lighter-weight one than production (an in-memory database instead of a real one, for example). Used when the test needs the dependency to behave realistically across a sequence of calls, not just answer one call in isolation.

A fast decision rule

Ask one question first: does this test care what my code returns given some input, or does it care whether my code called a dependency correctly?

  • If it's about the return value your code produces given some canned input from a dependency — reach for a stub.
  • If it's about whether a specific call happened (did checkout actually call charge() with the right amount, exactly once) — reach for a mock (or a spy, if you also want to inspect the stub's return value separately from asserting the call).
  • If the dependency needs to behave like a real, stateful system across several calls in the same test (add an item, then list items, and see the one you added) — a stub or mock gets awkward fast; a fake is usually less code overall, not more, because you're not manually wiring up expectations for every call.

The trap: over-mocking couples tests to implementation, not behavior

The most common way this goes wrong in practice isn't picking the wrong double once — it's defaulting to mocks everywhere, which quietly changes what your tests are actually protecting. A test full of verify(gateway).charge(42.00)-style assertions breaks the moment you refactor how charging happens internally, even if the externally-visible behavior is unchanged — which is exactly backward from what a good test suite should reward. As a rule of thumb: prefer asserting on outcomes (the return value, the resulting state) over asserting on interactions, and reach for interaction-verifying mocks specifically when the interaction itself is the thing you're testing (side effects with no observable return value — did we actually send the email, did we actually publish the event) rather than as a default habit.

Where this connects to the rest of this roadmap

This is deliberately scoped to interview-round fluency, not full testing strategy — a broader production-testing track (unit/integration/contract testing, testing distributed systems, CI pipeline design) is a plausible future addition to this site and would own that wider ground. What belongs here is narrower and more mechanical: recognizing which double a given test needs, fast, and being able to write it without reaching for documentation mid-round.

Reference implementations in:

The Same Dependency, Doubled Three Different Ways

A PaymentGateway dependency, tested with a stub (canned answer), a mock (verifies a call happened), and a fake (a real, simplified working implementation) — same collaborator, three different questions being asked of it.

# STUB: canned answer, no behavior, used when the test only cares about the RESULT class StubGateway: def charge(self, amount): return {"status": "success"} # MOCK: verifies a specific call happened -- used when the test cares about INTERACTION from unittest.mock import Mock mock_gateway = Mock() checkout(mock_gateway, cart) mock_gateway.charge.assert_called_once_with(42.00) # FAKE: a real, simplified working implementation -- used when the test needs REALISTIC behavior class FakeGateway: def __init__(self): self.balance = 0 def charge(self, amount): self.balance += amount return {"status": "success"}

The stub answers "what does checkout() return given this canned response"; the mock answers "did checkout() call charge() correctly"; the fake answers "does a sequence of real charges behave consistently" -- three different questions, three different tools.

Further Resources (Optional)