SwiftQARandom

What's the difference between mocks, stubs, spies, and fakes?

These four are all “test doubles” — stand-ins used in place of a real dependency so a unit can be tested in isolation. The differences come down to what they do when called, and what you check afterward.

Stub

The simplest one: a stub returns canned, hardcoded data when called, and you don’t really care whether or how it was called. If you’re testing a view model that fetches a user profile, a stub network client just returns a fixed User object no matter what request comes in.

Use it when: you need a dependency to return something specific so the code under test has data to work with, but the interaction itself isn’t what you’re verifying.

Mock

A mock is about verifying behavior, not just supplying data. You set expectations beforehand — “this method should be called exactly once, with these arguments” — and the test fails if that interaction doesn’t happen as expected.

Use it when: the thing you actually care about is whether your code called something correctly, e.g. verifying that saving a form calls analytics.track(.formSubmitted) exactly once.

Spy

A spy sits between a stub and a mock. It behaves like a real object (or wraps one) but records what happened — calls made, arguments passed — so you can make assertions after the fact rather than setting up expectations in advance.

Distinction from a mock: timing. Mocks assert expectations upfront and fail fast on violation; spies just record and let you inspect afterward. In practice, a lot of mocking frameworks blur mocks and spies together — it’s fine to acknowledge that while still being clear on the conceptual difference.

Fake

A fake is a working, simplified implementation rather than something that just returns canned values. It has real logic, just not production-grade. The classic example is an in-memory Core Data store or an in-memory dictionary standing in for a real database or Keychain — it behaves correctly (you can write and read back), it’s just not persistent or production-ready.

Use it when: the interaction is too complex for a stub to fake convincingly, but standing up the real dependency (network, disk, database) would make the test slow or flaky.

When to use which

Prefer stubs and fakes as the default, since they keep tests focused on state and outcomes rather than implementation details — stub for simple canned responses, fake when you need something that behaves realistically across multiple calls.

Reach for mocks and spies specifically when the behavior you’re testing is an interaction itself — did we call this delegate, did we send this analytics event, did we invoke completion exactly once.

A common critique worth mentioning: mocking too aggressively couples tests to implementation details, so refactoring internals breaks tests even when behavior is unchanged. That’s often the argument for preferring fakes over mocks where practical, especially for things like network or persistence layers in an iOS app.

Worked iOS example

Testing a LoginViewModel:

  • Stub the AuthService to return a fixed success/failure so you can assert on view state.
  • Use a fake in-memory KeychainStore so token persistence actually round-trips without touching the real Keychain.
  • Use a mock/spy on an AnalyticsLogging protocol to verify logLoginSuccess() was called once after a successful login.