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

XCTest中无需依赖Task.sleep等待跨全局Actor上下文异步任务完成的测试方案咨询

XCTest中无需依赖Task.sleep等待跨全局Actor上下文异步任务完成的测试方案咨询

你遇到的问题确实很典型——用同步方法触发异步Actor上下文的调用,测试时不想靠sleep这种“笨办法”等任务完成。核心痛点是MyProtocolImpl.execute()是同步的,但内部启动了一个Task在StreamActor上下文里调用异步代理的方法,测试需要精准等待这个异步调用完成,同时验证上下文和调用次数。

下面给你两种更优雅的解决方案,都是基于XCTest的异步测试机制或者Swift Concurrency的原生工具,完全不用Task.sleep:


方案1:用XCTestExpectation实现精准等待

这种方案利用XCTest原生的期望机制,让Spy在收到execute调用并完成时通知测试,测试等待这个通知后再做断言,比sleep可靠得多(timeout是合理的超时阈值,不是无意义的等待)。

步骤1:重构Spy,添加期望通知

修改你的AsyncMyProtocolSpy,让它接收一个XCTest期望,在execute调用完成时标记期望已满足:

private class AsyncMyProtocolSpy: AsyncMyProtocol {
    enum Messages: Equatable { case execute }
    // 用@StreamActor标记属性,确保在正确上下文修改/访问
    @StreamActor private(set) var receivedMessages: [Messages] = []
    private let executeCompletedExpectation: XCTestExpectation
    
    init(expectation: XCTestExpectation) {
        self.executeCompletedExpectation = expectation
    }
    
    func execute() async {
        @StreamActor in
        // 可选但推荐:验证是否在StreamActor上下文执行
        guard Task.currentActor is StreamActor else {
            XCTFail("execute 未在StreamActor上下文执行")
            return
        }
        
        receivedMessages.append(.execute)
        // 通知测试:异步调用已完成
        executeCompletedExpectation.fulfill()
    }
}

步骤2:修改测试方法,使用期望等待

func test_execute_callsExecuteOnAsnycProxyInStreamActorContext() async {
    // 1. 创建测试期望,指定要等待1次调用完成
    let executeExpectation = self.expectation(description: "异步代理的execute方法被调用并完成")
    executeExpectation.expectedFulfillmentCount = 1
    
    // 2. 初始化Spy和被测对象
    let spy = AsyncMyProtocolSpy(expectation: executeExpectation)
    let sut = MyProtocolImpl(asyncProxy: spy)
    
    // 3. 触发测试动作
    sut.execute()
    
    // 4. 等待期望完成(timeout设为合理值,比如1秒,比sleep更可控)
    await fulfillment(of: [executeExpectation], timeout: 1.0)
    
    // 5. 断言结果(注意要在StreamActor上下文访问属性,用await)
    let receivedMessages = await spy.receivedMessages
    XCTAssertEqual(receivedMessages, [.execute])
}

方案2:用CheckedContinuation实现Swift Concurrency风格的挂起等待

这种方案更贴合Swift Concurrency的异步编程模型,让测试主动挂起,直到Spy的execute调用完成,完全不用XCTest的期望API,代码更简洁。

步骤1:重构Spy,添加挂起等待逻辑

给Spy加一个方法,让测试可以挂起直到execute被调用完成:

private class AsyncMyProtocolSpy: AsyncMyProtocol {
    enum Messages: Equatable { case execute }
    @StreamActor private(set) var receivedMessages: [Messages] = []
    private var completionContinuation: CheckedContinuation<Void, Never>?
    
    // 测试调用这个方法挂起,直到execute完成
    func waitForExecuteCompletion() async {
        await withCheckedContinuation { [weak self] continuation in
            self?.completionContinuation = continuation
        }
    }
    
    func execute() async {
        @StreamActor in
        // 可选但推荐:验证是否在StreamActor上下文执行
        guard Task.currentActor is StreamActor else {
            XCTFail("execute 未在StreamActor上下文执行")
            completionContinuation?.resume()
            completionContinuation = nil
            return
        }
        
        receivedMessages.append(.execute)
        // 恢复测试的挂起状态
        completionContinuation?.resume()
        completionContinuation = nil
    }
}

步骤2:修改测试方法,直接挂起等待

func test_execute_callsExecuteOnAsnycProxyInStreamActorContext() async {
    // 1. 初始化Spy和被测对象
    let spy = AsyncMyProtocolSpy()
    let sut = MyProtocolImpl(asyncProxy: spy)
    
    // 2. 触发测试动作
    sut.execute()
    
    // 3. 挂起测试,直到execute调用完成(完全不用sleep或timeout,精准可靠)
    await spy.waitForExecuteCompletion()
    
    // 4. 断言结果
    let receivedMessages = await spy.receivedMessages
    XCTAssertEqual(receivedMessages, [.execute])
}

关键注意点

  1. Actor上下文的属性访问:因为AsyncMyProtocol标记了@StreamActor,所以Spy的所有方法和属性都在StreamActor上下文里,测试访问receivedMessages时必须用await(或者在@StreamActor闭包里访问),否则会有并发访问问题。
  2. 上下文验证:在Spy里用Task.currentActor is StreamActor可以直接验证调用是否在目标全局Actor上下文中,比靠sleep猜更可靠。
  3. 避免内存泄漏:方案2里用weak self捕获Spy的continuation,防止测试结束后Spy还持有continuation导致泄漏。

这两种方案都比Task.sleep靠谱得多,不会因为机器性能波动导致测试不稳定,也更符合Swift异步编程的最佳实践。

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.07 12:09:31