WWDC Quick Look 💓 By SwiftGGTeam
Go further with Swift Testing

Go further with Swift Testing

元の動画を見る

ハイライト

Swift Testing の #expect(throws:) マクロが do-catch 定型を 1 行に、withKnownIssue が期待失敗をレポートを汚さずマーク、パラメータ化テストが 1 関数で複数入力をカバー——この 3 機能が本セッションの核心。

主要内容

テスト作成でエラー処理は最も省略されがち。XCTest で throwing 関数をテストするには do-catch 手書き——冗長。関数が throw しなければ do-catch は「テストが効いていない」ことを教えてくれない。Swift Testing の #expect(throws:) が解決:エラー型を渡せば該当型 throw で合格、しなければ失敗——1 行。

もう一つの痛点:「既知の失敗テスト」。外部依存が壊れて一時修復不能でも毎回赤表示、本当の失敗を埋もれさせる。.disabled でスキップするとコンパイルエラーも見えなくなる。withKnownIssue はより精密:テストは実行、失敗は「期待失敗」として記録し失敗総数に含めない。修復時に削除を促す。

パラメータ化テストは重複コードを解決。enum に 6 case、各 containsNuts をテスト。6 関数コピペ?ミスしやすい。for ループ?1 失敗で中断、どの値か不明。@Test(arguments:) で入力を属性に、フレームワークが各引数で独立ケース生成、失敗時に入力を明示、全ケース並列実行。

詳細

エラーアサーション:#expect(throws:)

#expect(throws:) は 4 段階の精度(02:57):

// 何か throw すればよい
#expect(throws: (any Error).self) {
    try teaLeaves.brew(forMinutes: 200)
}

// 型を検証
#expect(throws: BrewingError.self) {
    try teaLeaves.brew(forMinutes: 200)
}

// 特定 case に一致
#expect(throws: BrewingError.oversteeped) {
    try teaLeaves.brew(forMinutes: 200)
}

// カスタム検証(関連値など)
#expect {
    try teaLeaves.brew(forMinutes: 3)
} throws: { error in
    guard let error = error as? BrewingError,
          case let .needsMoreTime(optimalBrewTime) = error else {
        return false
    }
    return optimalBrewTime == 4
}

キーポイント:

  • throws: (any Error).self — 何か throw すれば合格
  • throws: BrewingError.self — 別型 throw で失敗
  • throws: BrewingError.oversteeped — case 完全一致(関連値含む)
  • クロージャ形式 — 最柔軟。Bool 返却で判定

#require(throws:) は必須合格版(04:11):失敗時テスト即終了。

既知問題:withKnownIssue

@Test func softServeIceCreamInCone() throws {
    let iceCreamBatter = IceCreamBatter(flavor: .chocolate)
    try #require(iceCreamBatter != nil)
    #expect(iceCreamBatter.flavor == .chocolate)

    withKnownIssue {
        try softServeMachine.makeSoftServe(in: .cone)
    }
}

キーポイント(04:29):

  • withKnownIssue は失敗部分のみラップ、他アサーションは通常実行
  • 失敗は「期待失敗」記録、テスト失敗にカウントしない
  • 根本問題修復時に削除を促す
  • .disabled より優位:コンパイル・実行継続

パラメータ化テスト

@Test(arguments: [IceCream.Flavor.vanilla, .chocolate, .strawberry, .mintChip])
func doesNotContainNuts(flavor: IceCream.Flavor) throws {
    try #require(!flavor.containsNuts)
}

@Test(arguments: [IceCream.Flavor.rockyRoad, .pistachio])
func containNuts(flavor: IceCream.Flavor) {
   #expect(flavor.containsNuts)
}

キーポイント(08:44):

  • @Test(arguments:) は Sendable コレクション(配列、辞書、Range 等)を受け付け
  • 各引数が独立テストケース、失敗時に入力を明示
  • 全ケース並列——for ループより高速
  • 入力が Codable なら Xcode で失敗ケース単体再実行可

多パラメータ組み合わせ(11:07):

@Test(arguments: Ingredient.allCases, Dish.allCases)
func cook(_ ingredient: Ingredient, into dish: Dish) async throws { ... }

2 コレクションのデカルト積——4 食材 × 4 料理 = 16 ケース。全組み合わせ不要なら zip

@Test(arguments: zip(Ingredient.allCases, Dish.allCases))
func cook(_ ingredient: Ingredient, into dish: Dish) async throws { ... }

zip は順序ペア——4 ケースのみ。

Suite、Tag、並列テスト

@Suite("Various desserts")
struct DessertTests {
    @Suite struct WarmDesserts {
        @Test func applePieCrustLayers() { /* ... */ }
        @Test func lavaCakeBakingTime() { /* ... */ }
    }

    @Suite struct ColdDesserts {
        @Test func cheesecakeBakingStrategy() { /* ... */ }
        @Test func mangoSagoToppings() { /* ... */ }
    }
}

Suite はネスト可(13:11)——論理グループ化。

Tag は Suite 横断でテスト関連付け(13:36):

extension Tag {
    @Tag static var caffeinated: Self
    @Tag static var chocolatey: Self
}

@Suite(.tags(.caffeinated)) struct DrinkTests { ... }
@Test(.tags(.caffeinated, .chocolatey)) func espressoBrownieTexture() { ... }

Tag は Xcode 16 テストナビゲーター/レポートに表示。テストプランで Tag 包含/除外。分布インサイトで同一 Tag 失敗の共通点を発見。

並列テストはデフォルト有効(21:20)。実行順ランダムで暗黙データ依存を暴露。依存解決前は .serialized

@Suite("Cupcake tests", .serialized)
struct CupcakeTests { ... }

.serialized は子 Suite に継承——重複不要。

非同期コールバックテスト:confirmation

複数回発火するコールバックは手動カウントの代わりに confirmation25:50):

@Test func bakeCookies() async throws {
    let cookies = await Cookie.bake(count: 10)
    try await confirmation("Ate cookies", expectedCount: 10) { ateCookie in
        try await eat(cookies, with: .milk) { cookie, crumbs in
            #expect(!crumbs.in(.milk))
            ateCookie()
        }
    }
}

expectedCount で期待コールバック回数。不一致で失敗。0 で「決して発火しない」アサーション。

重要ポイント

  1. do-catch テストパターンを #expect(throws:) に置換なぜ価値があるか:do-catch は冗長で「throw しなかった」見落としあり。始め方:throw 検証の do-catch を #expect(throws: ErrorType.self) に。

  2. パラメータ化テストで重複テスト関数を撲滅なぜ価値があるか:同一ロジック・異入力のコピペはミス多い。入力とロジック分離、新 case は配列 1 行追加。始め方:入力のみ異なるテストを @Test(arguments:) で統合。

  3. withKnownIssue で「一時修復不能」失敗を管理なぜ価値があるか.disabled は完全スキップでコンパイルエラーも見えない。始め方:CI で繰り返し失敗するが当面放置するテストをラップ。

  4. Tag でファイル横断関連付け、テストプランで精密フィルタなぜ価値があるか:大規模化でディレクトリ構成だけでは不足。Tag でビジネス属性(有料機能、ネットワーク依存)横断。始め方:重要次元に Tag、テストプラン include/exclude でフィルタ。

関連セッション

コメント

GitHub Issues · utterances