Swift新并发模型下如何对依赖async函数的类做单元测试
Swift 新并发模型下测试ViewModel中间loading状态的方案
完全有和 completion handler、Combine 调度器等效的测试方案,核心逻辑和传统测试思路一致:通过测试替身控制异步依赖的执行时序,在目标挂起点插入断言即可,不需要引入额外第三方库。
方案1:可控挂起的Spy测试替身(最通用、零依赖)
这个方案和你之前用Spy捕获completion handler的逻辑完全对应,只是把存储回调换成存储Swift并发的续体(Continuation),手动控制异步方法的挂起/恢复时机。
首先实现ServiceType的测试替身:
@MainActor final class SpyService: ServiceType { /// 标记doSomething是否已经被调用,用来确认执行时序 private(set) var didCallDoSomething = false /// 存储异步续体,用于手动控制方法返回时机 private var continuation: CheckedContinuation<Void, Never>? func doSomething() async { didCallDoSomething = true // 调用后会一直挂起,直到主动调用resume触发恢复 await withCheckedContinuation { continuation in self.continuation = continuation } } /// 测试专用:手动触发异步方法返回,让后续业务逻辑继续执行 func resumeDoSomething() { continuation?.resume() continuation = nil } }
对应的单元测试代码(XCTest框架):
@MainActor func test_stateIsLoading_whenLoadCalledButAsyncTaskNotFinished() async { // 初始化测试依赖 let spyService = SpyService() let viewModel = ViewModel(service: spyService) // 单独开Task执行load方法,不能直接await,否则会等待整个方法执行完成 let loadTask = Task { await viewModel.load() } // 等待直到spy确认doSomething被调用:此时state已经被设为.loading,且正挂起在await service.doSomething()处 let timeout = Date().addingTimeInterval(1) while !spyService.didCallDoSomething { if Date() > timeout { XCTFail("异步方法未在预期时间内被调用") break } // 让出执行权,让loadTask推进执行 await Task.yield() } // 此时异步操作还未返回,断言中间状态符合预期 XCTAssertEqual(viewModel.state, .loading) // 手动恢复挂起的异步方法,让load流程执行完毕 spyService.resumeDoSomething() // 等待load任务结束,清理执行上下文 await loadTask.value // 可选:断言最终状态正确 XCTAssertEqual(viewModel.state, .loaded) }
注意事项
- 不要用
Task.sleep靠时间差碰运气等待状态,这种写法非常容易产生不稳定的flaky测试,必须用明确的时序信号(比如例子里的didCallDoSomething标记)确认已经走到目标代码位置。 - 如果你使用Swift Testing框架,可以直接用框架自带的
Confirmation或者Expectation替换例子里的轮询等待逻辑,写法会更简洁,核心控制逻辑完全一致。 - 这个场景不需要自定义全局Executor或者修改并发调度器,轻量的Spy实现就可以覆盖需求,维护成本最低。
内容的提问来源于stack exchange,提问作者Darren Findlay
相关产品推荐
相关产品推荐

