Swift中非异步函数调用异步函数的单元测试问题
解决方案与设计分析
测试适配方案(不修改协议、无人工等待)
方案1:依赖注入+XCTest期望触发
核心思路是通过替换MyClass依赖的异步方法实现,让测试能精准感知异步任务的完成时机:
- 先确保
MyClass的someAsyncFunc是可注入的(调整初始化逻辑,这不属于修改协议,只是优化实现的可测试性) - 在测试用例中创建
XCTestExpectation实例 - 注入自定义的
someAsyncFunc实现,在方法完成业务逻辑后调用expectation.fulfill() - 调用
myClass.doSomething() - 调用
waitForExpectations(timeout: 1)(timeout仅做兜底,实际会在任务完成后立即触发) - 此时再断言
didSomethingAsync标志,就能保证时机正确
示例代码:
func testDoSomethingAsyncFlag() { let expectation = XCTestExpectation(description: "Async task completes") let myClass = MyClass() // 替换依赖的异步方法(假设someAsyncFunc是可注入的闭包或协议类型) myClass.someAsyncFunc = { // 模拟原方法逻辑 myClass.didSomethingAsync = true expectation.fulfill() } myClass.doSomething() waitForExpectations(timeout: 1) XCTAssertTrue(myClass.didSomethingAsync) }
方案2:Debug模式下暴露内部Task引用
如果不想调整依赖注入逻辑,可以在MyClass中添加仅Debug模式可见的属性,存储内部启动的Task:
class MyClass: AppStartupService { var didSomethingAsync = false #if DEBUG var internalTask: Task<Void, Error>? #endif func doSomething() { #if DEBUG internalTask = Task { await someAsyncFunc() } #else Task { await someAsyncFunc() } #endif } private func someAsyncFunc() async { didSomethingAsync = true } }
测试中直接等待这个Task完成:
func testDoSomethingAsyncFlag() async { let myClass = MyClass() myClass.doSomething() // 等待内部Task执行完毕 if let task = myClass.internalTask { await task.value } XCTAssertTrue(myClass.didSomethingAsync) }
这个方案不会影响Release版本代码,也无需修改协议。
设计层面的潜在问题
你当前的“非异步方法启动异步任务且不等待”属于Fire-and-Forget模式,存在几个隐性问题:
- 可测试性差:调用者无法感知任务完成时机,也无法捕获异步任务中的错误
- 错误无处理路径:如果
someAsyncFunc执行出错,没有任何地方能捕获并处理这个错误,可能导致隐性故障 - 生命周期风险:如果
MyClass在异步任务完成前被销毁,可能出现内存泄漏或非法访问问题
如果业务场景允许,建议考虑优化方向:
- 给
AppStartupService协议添加回调方法,在异步任务完成时通知调用者 - 使用
AsyncStream让调用者能监听任务状态 - 若协议可调整,将
doSomething()改为异步方法(你当前限制不可修改协议,这条仅做参考)
内容的提问来源于stack exchange,提问作者SimonMoss
相关产品推荐
相关产品推荐

