axiom-testing-async について
axiom-testing-asyncは、Swift Testingを使ってSwiftの非同期コードをテストするためのガイドです。コールバック、アクター分離、並列実行、XCTestからの移行パターンを扱います。
主な機能
- 01非同期関数とthrows関数のテスト
- 02confirmationによるコールバック検証
- 03MainActorのテストパターン
- 04タイムアウトとパラメータ化テスト
- 05並列実行と共有状態の指針
- 06XCTestからの移行パターン
活用シーン
- 01async/await関数のテスト
- 02コールバック回数やイベントの検証
- 03MainActor分離されたビューモデルのテスト
- 04非同期テストのXCTestからの移行
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ charleswiltgen/ axiom --skill axiom-testing-asyncインストール後、Claude Code で/axiom-testing-asyncを入力して読み込まれたか確認します。
詳細
非同期コードのテスト — Swift Testingのパターン
Swift Testingフレームワークを使用してasync/awaitコードをテストするための最新のパターンです。
使用するタイミング
✅ 次の場合に使用します:
- 非同期関数のテストを作成するとき
- Swift TestingでコールバックベースのAPIをテストするとき
- 非同期のXCTestをSwift Testingに移行するとき
- MainActorで分離されたコードをテストするとき
- イベントが期待した回数だけ発生することを検証したいとき
❌ 次の場合は使用しないでください:
- XCTestのみを使用するプロジェクト(XCTestExpectationを使用してください)
- UI自動化テスト(XCUITestを使用してください)
- メトリクスを使ったパフォーマンステスト(XCTestを使用してください)
XCTestとの主な違い
| XCTest | Swift Testing |
|---|---|
XCTestExpectation | confirmation { } |
wait(for:timeout:) | await confirmation |
@MainActor 暗黙的 | @MainActor 明示的 |
| デフォルトで直列 | デフォルトで並列 |
XCTAssertEqual() | #expect() |
continueAfterFailure | 期待値ごとの #require |
パターン
パターン1:シンプルな非同期関数
@Test func fetchUser() async throws {
let user = try await api.fetchUser(id: 1)
#expect(user.name == "Alice")
}
パターン2:完了ハンドラー → 継続
非同期オーバーロードのないAPIの場合:
@Test func legacyAPI() async throws {
let result = try await withCheckedThrowingContinuation { continuation in
legacyFetch { result, error in
if let result {
continuation.resume(returning: result)
} else {
continuation.resume(throwing: error!)
}
}
}
#expect(result.isValid)
}
パターン3:confirmationを使った単一のコールバック
コールバックが正確に1回呼び出される場合:
@Test func notificationFires() async {
await confirmation { confirm in
NotificationCenter.default.addObserver(
forName: .didUpdate,
object: nil,
queue: .main
) { _ in
confirm() // Must be called exactly once
}
triggerUpdate()
}
}
パターン4:expectedCountを使った複数のコールバック
@Test func delegateCalledMultipleTimes() async {
await confirmation(expectedCount: 3) { confirm in
delegate.onProgress = { progress in
confirm() // Called 3 times
}
startDownload() // Triggers 3 progress updates
}
}
パターン5:コールバックが一度も呼び出されないことを検証する
@Test func noErrorCallback() async {
await confirmation(expectedCount: 0) { confirm in
delegate.onError = { _ in
confirm() // Should never be called
}
performSuccessfulOperation()
}
}
パターン6:MainActorのテスト
@Test @MainActor func viewModelUpdates() async {
let vm = ViewModel()
await vm.load()
#expect(vm.items.count > 0)
#expect(vm.isLoading == false)
}
パターン7:タイムアウトの制御
@Test(.timeLimit(.seconds(5)))
func slowOperation() async throws {
try await longRunningTask()
}
パターン8:スローのテスト
@Test func invalidInputThrows() async throws {
await #expect(throws: ValidationError.self) {
try await validate(input: "")
}
}
// Specific error
@Test func specificError() async throws {
await #expect(throws: NetworkError.notFound) {
try await api.fetch(id: -1)
}
}
パターン9:#requireを使ったオプショナルのアンラップ
@Test func firstVideo() async throws {
let videos = try await videoLibrary.videos()
let first = try #require(videos.first) // Fails if nil
#expect(first.duration > 0)
}
パターン10:パラメーター化された非同期テスト
@Test("Video loading", arguments: [
"Beach.mov",
"Mountain.mov",
"City.mov"
])
func loadVideo(fileName: String) async throws {
let video = try await Video.load(fileName)
#expect(video.isPlayable)
}
引数は自動的に並列で実行されます。
テストの並列実行
Swift Testingでは、XCTestとは異なり、デフォルトでテストが並列実行されます。
共有状態への対処
// ❌ Shared mutable state — race condition
var sharedCounter = 0
@Test func test1() async {
sharedCounter += 1 // Data race!
}
@Test func test2() async {
sharedCounter += 1 // Data race!
}
// ✅ Each test gets fresh instance
struct CounterTests {
var counter = Counter() // Fresh per test
@Test func increment() {
counter.increment()
#expect(counter.value == 1)
}
}
直列実行を強制する
テストを順番に実行する必要がある場合:
@Suite("Database tests", .serialized)
struct DatabaseTests {
@Test func createRecord() async { /* ... */ }
@Test func readRecord() async { /* ... */ } // After create
@Test func deleteRecord() async { /* ... */ } // After read
}
注:その他の無関係なテストは引き続き並列で実行されます。
よくある間違い
間違い1:confirmationの代わりにsleepを使う
// ❌ Flaky — arbitrary wait time
@Test func eventFires() async {
setupEventHandler()
try await Task.sleep(for: .seconds(1)) // Hope it happened?
#expect(eventReceived)
}
// ✅ Deterministic — waits for actual event
@Test func eventFires() async {
await confirmation { confirm in
onEvent = { confirm() }
triggerEvent()
}
}
間違い2:UIテストで@MainActorを付け忘れる
// ❌ Data race — ViewModel may be MainActor
@Test func viewModel() async {
let vm = ViewModel()
await vm.load() // May cause data race warnings
}
// ✅ Explicit isolation
@Test @MainActor func viewModel() async {
let vm = ViewModel()
await vm.load()
}
間違い3:コールバックにconfirmationを指定し忘れる
// ❌ Test passes immediately — doesn't wait for callback
@Test func callback() async {
api.fetch { result in
#expect(result.isSuccess) // Never executed before test ends
}
}
// ✅ Waits for callback
@Test func callback() async {
await confirmation { confirm in
api.fetch { result in
#expect(result.isSuccess)
confirm()
}
}
}
間違い4:並列実行に対処しない
// ❌ Tests interfere with each other
@Test func writeFile() async {
try! "data".write(to: sharedFileURL, atomically: true, encoding: .utf8)
}
@Test func readFile() async {
let data = try! String(contentsOf: sharedFileURL) // May fail!
}
// ✅ Use unique files or .serialized
@Test func writeAndRead() async {
let url = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString)
try! "data".write(to: url, atomically: true, encoding: .utf8)
let data = try! String(contentsOf: url)
#expect(data == "data")
}
XCTestからの移行
XCTestExpectation → confirmation
// XCTest
func testFetch() {
let expectation = expectation(description: "fetch")
api.fetch { result in
XCTAssertNotNil(result)
expectation.fulfill()
}
wait(for: [expectation], timeout: 5)
}
// Swift Testing
@Test func fetch() async {
await confirmation { confirm in
api.fetch { result in
#expect(result != nil)
confirm()
}
}
}
非同期setUp → Suiteの初期化
// XCTest
class MyTests: XCTestCase {
var service: Service!
override func setUp() async throws {
service = try await Service.create()
}
}
// Swift Testing
struct MyTests {
let service: Service
init() async throws {
service = try await Service.create()
}
@Test func example() async {
// Use self.service
}
}
リソース
WWDC:2024-10179、2024-10195
ドキュメント:/testing、/testing/confirmation
スキル:axiom-swift-testing、axiom-ios-testing
コメント
Markdown 対応