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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 21:48:14