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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 23:30:37