Mock vs Stub vs Fake vs Spy

You've heard these terms thrown around in testing discussions. Here's what they actually mean and when to use each.
Follow along with the code: iOS-Practice on GitHub
The Four Types
| Type | Purpose | Remembers Calls? | Has Logic? |
|---|---|---|---|
| Stub | Returns canned responses | No | No |
| Spy | Records method calls | Yes | No |
| Fake | Working implementation with shortcuts | Maybe | Yes |
| Mock | Stub + Spy combined | Yes | No |
Stub: Returns Canned Data
A stub just returns what you tell it to. No tracking, no logic.
class StubPaymentProcessor: PaymentProcessor {
var resultToReturn: PaymentResult = PaymentResult(
transactionId: "txn_123",
status: .success,
processedAt: Date()
)
func processPayment(amount: Decimal, currency: String, cardToken: String) async throws -> PaymentResult {
return resultToReturn
}
}
// Usage
func test_placeOrder_whenPaymentSucceeds() async throws {
let stub = StubPaymentProcessor()
stub.resultToReturn = PaymentResult(transactionId: "txn_abc", status: .success, processedAt: Date())
let sut = OrderService(paymentProcessor: stub, ...)
let result = try await sut.placeOrder(...)
XCTAssertNotNil(result)
}
Spy: Records Calls
A spy tracks what was called and with what arguments.
class SpyEmailService: EmailService {
struct SendEmailCall: Equatable {
let recipient: String
let subject: String
let body: String
}
private(set) var sendEmailCalls: [SendEmailCall] = []
func sendEmail(to recipient: String, subject: String, body: String) async throws {
sendEmailCalls.append(SendEmailCall(recipient: recipient, subject: subject, body: body))
}
var sendEmailCallCount: Int { sendEmailCalls.count }
func verifySendEmailCalled(with recipient: String) -> Bool {
sendEmailCalls.contains { $0.recipient == recipient }
}
}
// Usage
func test_placeOrder_sendsConfirmationEmail() async throws {
let spy = SpyEmailService()
let sut = OrderService(emailService: spy, ...)
_ = try await sut.placeOrder(customerEmail: "test@example.com", ...)
XCTAssertEqual(spy.sendEmailCallCount, 1)
XCTAssertTrue(spy.verifySendEmailCalled(with: "test@example.com"))
}
Fake: Working Implementation
A fake has real logic but takes shortcuts (like using memory instead of disk).
class FakeNotificationScheduler: NotificationScheduler {
private var notifications: [String: ScheduledNotification] = [:]
private var nextId = 1
func scheduleNotification(title: String, body: String, at date: Date) throws -> String {
let id = "notif_\(nextId)"
nextId += 1
notifications[id] = ScheduledNotification(id: id, title: title, body: body, scheduledDate: date)
return id
}
func cancelNotification(id: String) throws {
notifications.removeValue(forKey: id)
}
func getPendingNotifications() -> [ScheduledNotification] {
Array(notifications.values)
}
// Test helper
func hasNotification(withId id: String) -> Bool {
notifications[id] != nil
}
}
The InMemoryFileSystem and InMemoryKeyValueStore from previous posts are fakes.
Mock: Stub + Spy
Most test doubles in practice are mocks—they return canned data AND track calls.
class MockPaymentProcessor: PaymentProcessor {
// Stub behavior
var paymentResultToReturn: PaymentResult = PaymentResult(
transactionId: "txn_123",
status: .success,
processedAt: Date()
)
var errorToThrow: Error?
// Spy behavior
struct ProcessPaymentCall {
let amount: Decimal
let currency: String
let cardToken: String
}
private(set) var processPaymentCalls: [ProcessPaymentCall] = []
func processPayment(amount: Decimal, currency: String, cardToken: String) async throws -> PaymentResult {
processPaymentCalls.append(ProcessPaymentCall(amount: amount, currency: currency, cardToken: cardToken))
if let error = errorToThrow { throw error }
return paymentResultToReturn
}
// Helpers
func setDeclinedResponse(reason: String) {
paymentResultToReturn = PaymentResult(
transactionId: "txn_declined",
status: .declined(reason: reason),
processedAt: Date()
)
}
func reset() {
paymentResultToReturn = PaymentResult(transactionId: "txn_123", status: .success, processedAt: Date())
errorToThrow = nil
processPaymentCalls = []
}
}
When to Use Each
- Stub: You only care about the return value
- Spy: You need to verify something was called correctly
- Fake: You need working behavior but want to avoid real I/O
- Mock: You need both return values and call verification (most common)
In practice, most test doubles are mocks. The distinction matters more for understanding intent than strict categorization.