用 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,就會後悔沒有早點改用它。
熟悉今天整理的四種模式後,大多數非同步測試都能順利涵蓋。請輕鬆地逐一套用,我會為你加油!

