Swift async/await如何实现类似TestScheduler的测试时间控制
Swift async/await 场景下实现手动控制测试时间的方案
完全有成熟方案可以实现和Combine时代TestScheduler一致的手动控时效果,甚至你之前用的TestSchedulers的原作者团队就做了async/await原生适配的实现,使用逻辑和你熟悉的API几乎没有学习成本。
核心思路和Combine时代抽象Scheduler协议的逻辑完全一致:
- 生产代码里不要直接硬依赖系统提供的实时时钟,把所有时间相关操作(比如
Task.sleep、超时判断、定时任务)依赖的时钟抽象成可注入的Clock协议类型,默认赋值为系统原生的ContinuousClock保证线上逻辑正常。 - 测试环境下把注入的时钟替换成可手动控制的测试用Clock实现,就能做到和
queue.advance()完全一样的效果:想卡在哪一步就卡在哪一步,想推进多少时间就推进多少时间,不会出现阻塞、实时等待的问题。
具体用法示例
第一步:生产代码做依赖注入
不要在业务逻辑里直接调用不可控的系统时间API,把时钟作为参数传入:
// 被测视图模型示例 struct ContentViewModel { enum ContentState: Equatable { case notAsked, loading, loaded(String), failed } var content: ContentState = .notAsked // 注入时钟依赖,默认用系统实时时钟 private let clock: any Clock<Duration> private let fetchService: FetchServiceProtocol init( clock: any Clock<Duration> = ContinuousClock(), fetchService: FetchServiceProtocol ) { self.clock = clock self.fetchService = fetchService } func fetchContent() async { content = .loading do { let data = try await fetchService.fetch() // 比如常见的loading最小展示时长逻辑,所有等待都走注入的clock try await clock.sleep(for: .milliseconds(300)) content = .loaded(data) } catch { content = .failed } } }
第二步:测试中替换为可控测试时钟
测试时不需要等真实时间流逝,手动推进时钟即可在任意节点断言状态:
func testFetchContentShowLoadingState() async { // 初始化测试时钟,初始时间可自定义 let testClock = TestClock() // 注入Mock服务和测试时钟 let mockService = MockFetchService(returnData: "测试内容") let sut = ContentViewModel(clock: testClock, fetchService: mockService) // 初始状态断言 XCTAssertEqual(sut.content, .notAsked) // 触发异步请求,把任务单独持有避免提前跑完 let fetchTask = Task { await sut.fetchContent() } // 让出线程让同步执行的状态变更(也就是把content设为loading的逻辑)跑完 await Task.yield() // 这里可以稳定断言loading状态,不会因为Mock接口返回太快直接跳到最终结果 XCTAssertEqual(sut.content, .loading) // 等价于Combine时代的queue.advance(),手动推进300毫秒,触发所有等待中的续体执行 await testClock.advance(by: .milliseconds(300)) // 等待任务执行完成 await fetchTask.value // 断言最终加载结果 XCTAssertEqual(sut.content, .loaded("测试内容")) }
补充说明
- 这类测试时钟除了支持按固定步长推进时间,也支持直接跳转到指定时间点、检查当前是否存在挂起的等待任务,能力完全覆盖之前Combine的TestScheduler,测试全程不需要真实等待时间流逝,执行速度和普通单元测试一样快。
- 如果不想引入第三方实现,自己写一个最简可用的TestClock成本也很低:只要遵守Swift标准库的
Clock协议,内部维护一个挂起任务队列,调用advance时按时间顺序把到期的任务依次恢复即可,核心逻辑不到100行就能满足大部分单测场景。 - 注意所有时间相关的逻辑都必须走注入的时钟,不要在业务代码里直接调用
Task.sleep、Date()这类直接依赖系统实时状态的API,否则测试时钟就没法接管时间控制了。
内容的提问来源于stack exchange,提问作者Fogmeister
相关产品推荐
相关产品推荐

