Swift中Mock与Spy的核心差异是什么?关于测试实现差异的困惑咨询
Great question—this is such a common point of confusion when working with test doubles in Swift, especially since the lines can feel blurry without dedicated mocking tools. Let’s break this down clearly:
First, the Theoretical Difference
Let’s start with the classic definitions to set the baseline:
- Spy: A test double that records indirect outputs (like method call counts, passed parameters, or returned values). It doesn’t validate anything on its own—you, the tester, write assertions in your test case to check the recorded data. Think of it as a "spy" that watches and reports back, but doesn’t judge if the behavior was correct.
- Mock: A test double that does two things: it records behavior and has built-in expectations. You tell the mock upfront what behavior it should expect (e.g., "this method should be called exactly twice with parameter 'foo'"), and the mock itself validates whether those expectations were met. If not, it fails the test directly—no need for you to write separate assertions in the test case.
The Swift Reality (Without Third-Party Libraries)
Here’s where the confusion hits: Swift’s standard library doesn’t include built-in verify() functionality or a native mocking framework. So most "Mock" examples you see in Swift are actually Spies in disguise.
These examples use protocols or inheritance to create a double that records calls, but then you have to write XCTAssertXXX() in your test case to check the recorded data. That’s Spy behavior, not Mock behavior.
How to Create a "Real" Mock in Swift
To make a true Mock (one that self-validates), you need to build in expectation and verification logic yourself. Here’s a quick example:
Step 1: Define the Protocol
protocol DataFetcherProtocol { func fetchData(for id: String) async }
Step 2: Build the Self-Validating Mock
class DataFetcherMock: DataFetcherProtocol { // Track expectations private var expectedFetchCalls: Int = 0 private var expectedFetchID: String? // Track actual behavior private var actualFetchCalls: Int = 0 private var actualFetchIDs: [String] = [] // Set expectations func expectFetchCalls(count: Int, withID id: String? = nil) { expectedFetchCalls = count expectedFetchID = id } // Self-validation method func verify() throws { // Check call count guard actualFetchCalls == expectedFetchCalls else { throw MockError.unexpectedCallCount(expected: expectedFetchCalls, actual: actualFetchCalls) } // Check parameter if expected if let expectedID = expectedFetchID { guard actualFetchIDs.contains(expectedID) else { throw MockError.unexpectedParameter(expected: expectedID, actual: actualFetchIDs) } } } // Conform to the protocol func fetchData(for id: String) async { actualFetchCalls += 1 actualFetchIDs.append(id) } } // Custom error for verification failures enum MockError: Error, LocalizedError { case unexpectedCallCount(expected: Int, actual: Int) case unexpectedParameter(expected: String, actual: [String]) var errorDescription: String? { switch self { case .unexpectedCallCount(let expected, let actual): return "Expected \(expected) fetch calls, got \(actual)" case .unexpectedParameter(let expected, let actual): return "Expected fetch with ID '\(expected)', but got IDs: \(actual)" } } }
Step 3: Use the Mock in a Test
func testDataFetch() async throws { let mock = DataFetcherMock() mock.expectFetchCalls(count: 1, withID: "123") // Use the mock in your system under test let viewModel = DataViewModel(fetcher: mock) await viewModel.loadData(for: "123") // Let the mock validate itself try mock.verify() }
In this case, the Mock handles the validation internally. If the expectations aren’t met, it throws an error that fails the test—no need for XCTAssert calls in the test case.
Key Takeaways for Swift
- Most "Mocks" you see in Swift examples are actually Spies because they rely on test-case assertions to validate behavior.
- A true Mock in Swift requires you to implement expectation-setting and self-verification logic (or use a third-party library that does this for you).
- The core difference boils down to where validation happens:
- Spy: Validation is in the test case (using
XCTAssertXXX). - Mock: Validation is inside the test double itself (using a
verify()method or similar).
- Spy: Validation is in the test case (using
Third-party libraries like Cuckoo, Mockolo, or Nimble can automate the creation of true Mocks with verify() functionality, making this distinction much clearer in practice.
内容的提问来源于stack exchange,提问作者InTaek Cho

