How do you mock dependencies in Swift tests? What is the difference between a stub, a mock, a fake and a spy?
MediumAsked oftenTestingArchitecture 2 min read
TL;DR
Swift has no runtime mocking like Mockito. You mock by design:
- Put the dependency behind a protocol or a struct of closures.
- Inject it through
init. - In tests, pass a test double.
Kinds of test doubles: stub (returns fixed data), spy (records calls), mock (checks expected calls), fake (a simple working version).
Step 1: a protocol
protocol WeatherService: Sendable {
func temperature(in city: String) async throws -> Double
}
@MainActor @Observable
final class WeatherModel {
private(set) var text = ""
private let service: WeatherService
init(service: WeatherService) { self.service = service }
func load(city: String) async {
do {
let t = try await service.temperature(in: city)
text = "\(Int(t))°"
} catch {
text = "Unavailable"
}
}
}Step 2: test doubles
Stub: returns what you tell it
struct StubWeatherService: WeatherService {
var result: Result<Double, Error>
func temperature(in city: String) async throws -> Double {
try result.get()
}
}Spy: records what happened
actor SpyWeatherService: WeatherService {
private(set) var requestedCities: [String] = []
func temperature(in city: String) async throws -> Double {
requestedCities.append(city)
return 20
}
}Fake: a lightweight real implementation
final class InMemoryNotesStore: NotesStore {
private var notes: [Note] = []
func save(_ note: Note) { notes.append(note) }
func all() -> [Note] { notes }
}Mock: knows what calls to expect and verifies them
A mock is a spy plus built-in checks, often from a library. In Swift, most teams just use a spy and assert in the test.
Step 3: tests
import Testing
@MainActor
struct WeatherModelTests {
@Test func showsTemperature() async {
let model = WeatherModel(service: StubWeatherService(result: .success(21.6)))
await model.load(city: "Oslo")
#expect(model.text == "21°")
}
@Test func showsErrorText() async {
let model = WeatherModel(service: StubWeatherService(result: .failure(URLError(.timedOut))))
await model.load(city: "Oslo")
#expect(model.text == "Unavailable")
}
@Test func asksForTheRightCity() async {
let spy = SpyWeatherService()
let model = WeatherModel(service: spy)
await model.load(city: "Lima")
#expect(await spy.requestedCities == ["Lima"])
}
}Closures instead of protocols
Less code. Each test overrides only what it needs.
struct WeatherClient: Sendable {
var temperature: @Sendable (String) async throws -> Double
}
let client = WeatherClient(temperature: { _ in 30 })Mocking the network layer
To test your real API client without a server, use a custom URLProtocol:
final class MockURLProtocol: URLProtocol {
nonisolated(unsafe) static var handler: ((URLRequest) -> (HTTPURLResponse, Data))?
override class func canInit(with request: URLRequest) -> Bool { true }
override class func canonicalRequest(for request: URLRequest) -> URLRequest { request }
override func startLoading() {
guard let (response, data) = Self.handler?(request) else { return }
client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
client?.urlProtocol(self, didLoad: data)
client?.urlProtocolDidFinishLoading(self)
}
override func stopLoading() {}
}
let config = URLSessionConfiguration.ephemeral
config.protocolClasses = [MockURLProtocol.self]
let session = URLSession(configuration: config)This tests URL building, headers and decoding together. Because the handler is shared, run these tests serialized.
Things to inject besides services
- Time: a
Clockor anow: () -> Dateclosure. Never realsleepin tests. - UUIDs and random values.
- UserDefaults: use
UserDefaults(suiteName:)with a unique name per test. - File locations: a temporary directory.
Common mistakes
- Mocking value types or pure functions. Just call them.
- Testing the mock instead of the real code.
- Too many protocols "just in case". Add one when you need a second implementation.
- Over-checking calls (exact order, exact count) when only the result matters. Tests become fragile.
Common follow-up questions
- Are there mocking libraries? Yes, for example Mockolo or Cuckoo, which generate mocks from protocols. Many teams write doubles by hand.
- How do you mock a singleton? Hide it behind a protocol and inject
.sharedas the default argument. - How do you mock
Date()? Inject a closure or aClockand pass a fixed value in tests. - Why make the spy an actor? It is called from async code. An actor keeps its recorded data safe in Swift 6.