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

iOS单元测试中如何验证异步函数未被调用?

验证异步函数未被调用的XCTest方案

要验证异步函数未被调用,核心思路是通过Mock对象跟踪每个函数的调用次数,配合XCTestExpectation确保目标异步操作完成后,再断言其余函数的调用计数为0。针对你的Launcher场景,具体实现步骤如下:

1. 抽象协议(解耦测试与实现)

先给Launcher定义协议,这样可以用Mock类替代真实实现,方便跟踪调用:

protocol LauncherProtocol {
    func functionA(param: String, completion: @escaping () -> Void)
    func functionB(param: String, completion: @escaping () -> Void)
    func functionC(param: String, completion: @escaping () -> Void)
    func functionD(param: String, completion: @escaping () -> Void)
    func launch(with param: String)
}

2. 实现Mock类(记录调用次数)

创建Mock类实现上述协议,给每个函数添加调用计数器,同时在目标函数中触发测试预期:

class LauncherMock: LauncherProtocol {
    // 记录每个函数的调用次数
    var funcACalledCount = 0
    var funcBCalledCount = 0
    var funcCCalledCount = 0
    var funcDCalledCount = 0
    
    // 关联测试预期,用于等待目标函数完成
    var targetExpectation: XCTestExpectation?

    func functionA(param: String, completion: @escaping () -> Void) {
        funcACalledCount += 1
        // 模拟异步执行,完成后触发预期
        DispatchQueue.global().async {
            completion()
            self.targetExpectation?.fulfill()
        }
    }

    func functionB(param: String, completion: @escaping () -> Void) {
        funcBCalledCount += 1
        DispatchQueue.global().async { completion() }
    }

    func functionC(param: String, completion: @escaping () -> Void) {
        funcCCalledCount += 1
        DispatchQueue.global().async { completion() }
    }

    func functionD(param: String, completion: @escaping () -> Void) {
        funcDCalledCount += 1
        DispatchQueue.global().async { completion() }
    }

    // 模拟真实Launcher的触发逻辑:参数为"specific"时仅调用functionA
    func launch(with param: String) {
        if param == "specific" {
            functionA(param: param, completion: {})
        }
        // 其他参数逻辑省略
    }
}

3. 编写单元测试

在测试用例中,初始化Mock对象、设置预期、触发逻辑后,等待目标函数完成,最后断言计数:

class LauncherTests: XCTestCase {
    func testLaunchWithSpecificParamCallsOnlyFunctionA() {
        let mockLauncher = LauncherMock()
        // 创建针对目标函数的测试预期
        let expectation = self.expectation(description: "FunctionA should be invoked")
        mockLauncher.targetExpectation = expectation

        // 触发Launcher的启动逻辑
        mockLauncher.launch(with: "specific")

        // 等待预期完成,超时时间设为足够覆盖异步操作的时长
        waitForExpectations(timeout: 2) { error in
            if let error = error {
                XCTFail("Wait failed: \(error.localizedDescription)")
            }
        }

        // 验证目标函数被调用1次,其余函数未被调用(计数为0)
        XCTAssertEqual(mockLauncher.funcACalledCount, 1)
        XCTAssertEqual(mockLauncher.funcBCalledCount, 0)
        XCTAssertEqual(mockLauncher.funcCCalledCount, 0)
        XCTAssertEqual(mockLauncher.funcDCalledCount, 0)
    }
}

关键注意事项

  • 超时时间设置:要保证超时时间足够长,能覆盖所有可能的异步操作执行时长,避免因异步未完成导致误判。
  • Mock的异步模拟:尽量贴近真实实现的异步逻辑(比如用DispatchQueue),避免测试与实际行为脱节。
  • 协议解耦:使用协议而非直接依赖真实Launcher类,让测试更灵活,也符合依赖倒置原则。

内容的提问来源于stack exchange,提问作者Lucy Gu

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.28 14:52:40