测试与代码质量

如何测试 Swift async/await 异步代码

在 Swift 5.5 及更高版本中,将测试函数声明为 async,使用 await 等待结果,然后进行验证。本文还整理了错误验证、旧式回调 API 测试,以及仍需使用 expectation 的情况。

3 分钟阅读
如何测试 Swift async/await 异步代码 封面图

用 async/await 编写代码已经得心应手,但真正开始写测试时,是否也曾感到无从下手?

如果还保留着给 XCTestExpectation 添加 wait(for:)、测试回调地狱的习惯,这种感觉会尤其明显。

先说结论。使用 Swift 5.5 或更高版本(Xcode 13+)时,将 **测试函数本身声明为 async,通过 await等待结果后,再使用 **进行验证即可。过去使用 expectation 和超时的方式,现在大多数情况下都不再需要。

今天我会从头到尾讲解自己在实际工作中整理的 async/await 测试方法。

先总结核心要点

为了方便时间紧张的读者,先指出本文的核心内容。

  1. 给测试方法添加 async throws,并在其中调用 await
  2. 错误验证可以使用 do-catch,或采用 XCTAssertThrowsError的 async 版本模式
  3. 只有在确实需要超时时,才配合使用 expectation
  4. withCheckedThrowingContinuation 包装旧式回调 API 后再进行测试

如何测试 Swift async/await?

先从最基本的形式开始。

以前为了等待异步结果,我们会创建 expectation,并在闭包中调用 fulfill。代码冗长,也很难阅读。

现在简洁多了。

// 只需给测试函数添加 async throws即可
func test_加载用户_() async throws {
    let service = UserService()
    let user = try await service.fetchUser(id: 1)
    XCTAssertEqual(user.name, "李硕宇")
}

上面代码中需要注意的只有两点。

第一,函数声明中添加了 async throws;第二,用 try await等待结果后,再像平常一样通过 XCTAssertEqual进行验证。

让测试函数本身变成 async 后,异步代码也能像同步代码一样从上到下阅读。

改用这种方式后,我的测试代码行数减少了将近一半。

突出显示 await 关键字的 Swift async 测试函数和绿色勾选标记
async 函数在测试中也写成 async,就这么简单

发生错误时该如何验证?

我们也经常需要测试异步函数抛出错误的情况。

最直观的方法是 do-catch

func test_不存在的_用户_错误() async {
    let service = UserService()
    do {
        _ = try await service.fetchUser(id: -1)
        XCTFail("正常情况应该抛出错误")
    } catch {
        XCTAssertTrue(error is UserError)
    }
}

关键是在成功用例中加入 XCTFail

如果没有发生错误而是直接通过,测试看起来就像是悄悄成功了。可以把它看作防止这种情况的安全措施。

顺便一提,同步代码中使用的 XCTAssertThrowsError默认无法直接接收 async 函数。因此我更常像上面一样使用 do-catch展开编写。


如何测试旧式回调 API?

如果所有代码都已经迁移到 async 当然最好,但现实并非如此。

项目中应该还保留着一些通过 completion 处理程序返回结果的旧式 API。

这时可以用 withCheckedThrowingContinuation进行包装,把它带入 async 世界。

func fetchLegacy() async throws -> Data {
    try await withCheckedThrowingContinuation { continuation in
        oldAPI { data, error in
            if let error { continuation.resume(throwing: error) }
            else { continuation.resume(returning: data!) }
        }
    }
}

包装一次后,测试端只需通过 try await fetchLegacy()调用即可,因此和前面介绍的方法完全相同。

有一点需要注意:continuation 必须恰好resume一次。调用两次会导致崩溃,完全不调用则会让测试永远卡住。


expectation 方式和 async 方式,什么时候该用哪个?

下面整理了两者的对比。(Xcode 13 及更高版本,按 2026 年计算)

项目 expectation 方式 async 测试方式
代码长度
可读性 回调嵌套 从上到下顺序执行
设置超时 简单 需要单独处理
推荐场景 等待通知或计时器 大多数异步操作

如果是一般的 async/await 函数测试,async 方式方便得多。

不过,在确认通知是否能在指定时间内到达、计时器是否按时运行等 明确超时很重要的场景中,配合使用 expectation 仍然有效。

Xcode 测试导航器中显示 Test Passed 绿色勾选标记的画面
看到绿灯亮起的瞬间,总是最令人满足

总结

即使一开始不习惯,只要在测试函数中添加一次async throws,你就会想知道为什么没早点改。

熟悉今天总结的四种模式后,大多数异步测试都能轻松覆盖。放松下来,逐个尝试吧,我为你加油!