async/awaitでコードを書くのには慣れたのに、いざテストを書こうとすると途方に暮れたことはありませんか?
XCTestExpectationにwait(for:)を付けながらコールバック地獄をテストしていた頃の習慣が残っていると、特にそう感じます。
結論から言います。Swift 5.5以降(Xcode 13+)なら、テスト関数そのものをasyncとして宣言し、awaitで結果を待ってからで検証すれば完了です。expectationとタイムアウトを使う従来の方法は、ほとんど必要なくなりました。
今日は、実務で整理したasync/awaitのテスト方法を最初から最後まで解説します。
まずは要点をまとめます
忙しい方のために、この記事のポイントだけ押さえます。
- テストメソッドに
async throwsを付け、内部でawaitを呼び出す - エラー検証には
do-catchまたはXCTAssertThrowsErrorのasync版パターンを使う - タイムアウトが本当に必要な場合だけ
expectationを併用する - 古いコールバックAPIは
withCheckedThrowingContinuationでラップしてテストする
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を一度付けてみれば、なぜもっと早く変えなかったのかと思うはずです。
今日まとめた4つのパターンを身に付ければ、ほとんどの非同期テストを無理なくカバーできます。気軽に一つずつ試してみてください。応援しています!

