用 async/await 编写代码已经得心应手,但真正开始写测试时,是否也曾感到无从下手?
如果还保留着给 XCTestExpectation 添加 wait(for:)、测试回调地狱的习惯,这种感觉会尤其明显。
先说结论。使用 Swift 5.5 或更高版本(Xcode 13+)时,将 **测试函数本身声明为 async,通过 await等待结果后,再使用 **进行验证即可。过去使用 expectation 和超时的方式,现在大多数情况下都不再需要。
今天我会从头到尾讲解自己在实际工作中整理的 async/await 测试方法。
先总结核心要点
为了方便时间紧张的读者,先指出本文的核心内容。
- 给测试方法添加
async throws,并在其中调用await - 错误验证可以使用
do-catch,或采用XCTAssertThrowsError的 async 版本模式 - 只有在确实需要超时时,才配合使用
expectation - 用
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 后,异步代码也能像同步代码一样从上到下阅读。
改用这种方式后,我的测试代码行数减少了将近一半。
发生错误时该如何验证?
我们也经常需要测试异步函数抛出错误的情况。
最直观的方法是 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 仍然有效。
总结
即使一开始不习惯,只要在测试函数中添加一次async throws,你就会想知道为什么没早点改。
熟悉今天总结的四种模式后,大多数异步测试都能轻松覆盖。放松下来,逐个尝试吧,我为你加油!

