mcpField
A
axiom-testing-async

axiom-testing-async

github.com/charleswiltgenテスト・品質1.2k176 回インストール今日更新
リポジトリ

axiom-testing-async について

axiom-testing-asyncは、Swift Testingを使ってSwiftの非同期コードをテストするためのガイドです。コールバック、アクター分離、並列実行、XCTestからの移行パターンを扱います。

主な機能

  1. 01非同期関数とthrows関数のテスト
  2. 02confirmationによるコールバック検証
  3. 03MainActorのテストパターン
  4. 04タイムアウトとパラメータ化テスト
  5. 05並列実行と共有状態の指針
  6. 06XCTestからの移行パターン

活用シーン

  1. 01async/await関数のテスト
  2. 02コールバック回数やイベントの検証
  3. 03MainActor分離されたビューモデルのテスト
  4. 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との主な違い

XCTestSwift Testing
XCTestExpectationconfirmation { }
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

コメント