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]) }
关键注意点
- Actor上下文的属性访问:因为
AsyncMyProtocol标记了@StreamActor,所以Spy的所有方法和属性都在StreamActor上下文里,测试访问receivedMessages时必须用await(或者在@StreamActor闭包里访问),否则会有并发访问问题。 - 上下文验证:在Spy里用
Task.currentActor is StreamActor可以直接验证调用是否在目标全局Actor上下文中,比靠sleep猜更可靠。 - 避免内存泄漏:方案2里用
weak self捕获Spy的continuation,防止测试结束后Spy还持有continuation导致泄漏。
这两种方案都比Task.sleep靠谱得多,不会因为机器性能波动导致测试不稳定,也更符合Swift异步编程的最佳实践。
内容来源于stack exchange
相关产品推荐
相关产品推荐

