測試與程式碼品質

如何測試 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,就會後悔沒有早點改用它。

熟悉今天整理的四種模式後,大多數非同步測試都能順利涵蓋。請輕鬆地逐一套用,我會為你加油!