What is the difference between dummies, stubs, fakes, spies and mocks, and how do you decide which to use?

22.4.1 · L3 · Interview

Predict first

A sign-up service uses a fake user store, a spy mailer and a failing mailer stub. What do the two calls print?

import Foundation

protocol Mailer { func send(to: String, subject: String) throws }
protocol Users { func exists(_ email: String) -> Bool; func add(_ email: String) }

struct SignUp {
    let users: Users, mailer: Mailer
    func register(_ email: String) -> String {
        if users.exists(email) { return "duplicate" }
        users.add(email)
        do { try mailer.send(to: email, subject: "Welcome"); return "ok" } catch { return "saved, email failed" }
    }
}

final class FakeUsers: Users {                      // a fake: a real, simple implementation
    var set: Set<String> = []
    func exists(_ e: String) -> Bool { set.contains(e) }
    func add(_ e: String) { set.insert(e) }
}
final class SpyMailer: Mailer {                     // a spy: records what it was asked to do
    var sent: [String] = []
    func send(to: String, subject: String) throws { sent.append("\(subject)->\(to)") }
}
struct FailingMailer: Mailer {                      // a stub: returns a canned failure
    struct Down: Error {}
    func send(to: String, subject: String) throws { throw Down() }
}

let users = FakeUsers(), spy = SpyMailer()
let service = SignUp(users: users, mailer: spy)
print(service.register("a@x.com"), service.register("a@x.com"), spy.sent)
print(SignUp(users: FakeUsers(), mailer: FailingMailer()).register("b@x.com"))
  • ok duplicate ["Welcome->a@x.com"], then "saved, email failed"
  • ok ok with two emails, then "ok"
  • duplicate duplicate, then "ok"

Mental model. A flight simulator is a fake: it behaves like the real aircraft for training. A crash-test dummy is a spy: it records what happened to it. Neither is the plane, so you still need a real flight now and then.

These are all test doubles: stand-ins for a real collaborator, made possible by depending on a protocol (or a closure) instead of a concrete type, with the real one passed in through the initialiser (Q355). The common vocabulary: a dummy fills a parameter and is never used; a stub returns canned answers (a mailer that always throws, to test the error path); a fake is a working but simplified implementation (an in-memory user store instead of a database); a spy records how it was called so you can assert afterwards (the mailer saw “Welcome” sent once); a mock is pre-programmed with expectations and fails itself if they are not met. In Swift people often say “mock” for all of them. The check below uses a fake store, a spy mailer and a failing stub, and tests behaviour through them.

Choose by what you assert. Prefer fakes and stubs and assert on outcomes (state): “the user exists, the result is ok”. Use a spy when the interaction is the behaviour (an analytics event must be sent, a notification scheduled). Be wary of strict mocks that verify exact call sequences: they couple the test to the implementation and break on refactors (see the question on what not to test). Do not mock what you do not own in the middle of a test (types from Foundation or UIKit); wrap it in a small protocol you do own, and test that wrapper once against the real thing. Swift has no runtime mocking, so doubles are hand-written or generated (macros and libraries can generate them), which is a reason to keep protocols small. And keep at least some integration tests with the real pieces, so the doubles cannot drift from reality.

The second registration is rejected before any email, so the spy saw one message. The failing stub exercises the error path.

Common traps

  • Mocking everything, including value types and your own pure functions. The test then checks the mock, not the code.
  • Verifying every call in order. The test passes only for today’s implementation and fails on a harmless refactor.

They might follow up with

  • Fake or mock for a database? A fake (an in-memory store) lets you assert on resulting state instead of calls.
  • How do you keep doubles honest? Share a contract test that runs against both the real implementation and the fake.

Related questions

  • 22.4.2 What makes a test non-deterministic, and how do you control time zones, locales, randomness, UUIDs and the…
  • 22.1.2 What should you not test, and how do you decide whether a test is worth having?
  • 15.2.2 How do you do dependency injection in Swift, and when do you need a container or framework?
  • 12.6.2 How would you design a networking layer shared by many apps and teams: interceptors, mocking, and URLProtocol?

What makes a test non-deterministic, and how do you control time… →