You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Swift中Mock与Spy的核心差异是什么?关于测试实现差异的困惑咨询

Mock vs Spy in Swift: Clarifying the Confusion

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).

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.28 09:17:28