Xcodeで各シナリオに独自の結果と再実行コントロールを持たせるには、ループベースのXCTestユニットテストをSwift Testingのパラメーター化された@Testに移行します。この手順では、ローカルなタイル配置計算を使って移行方法を示し、結果の報告動作を確認します。

XCTestのループでは、なぜ各シナリオが個別に報告されないのでしょうか?

XCTestは、XCTestCaseサブクラスのテストメソッドを検出します。これらのメソッドはパラメーターを取らず、名前はtestで始まります。検出されたメソッド内のループはそのメソッドの一部のままであり、各反復が個別に検出されるテストになるわけではありません。アサーションにシナリオ名を加えると診断しやすくなりますが、個別の実行コントロールは作成されません。Appleのテストケースとテストメソッドの定義を参照してください。

ライフサイクルも同じ境界に従います。XCTestは各テストメソッドの前にインスタンスのセットアップメソッドを実行し、その後にティアダウンメソッドを実行します。そのため、5回の反復を行うループで、セットアップとティアダウンが自動的に5回ずつ実行されるわけではありません。ある反復でフィクスチャを変更すると、テストが明示的にリセットしない限り、次の反復がその状態を引き継ぐ可能性があります。Appleはこの順序をテストの状態をセットアップしてティアダウンするで説明しています。

Swift Testingは、ここで必要な結果を提供します。1つの関数がシナリオ引数を受け取り、ランナーはその呼び出しを個別のケースとして扱います。Xcodeには各ケースの引数が表示され、個別の実行コントロールが用意されています。Appleはこの動作をSwift Testingをさらに活用するで紹介しています。

この違いは、失敗を診断するときに重要です。アサーションの失敗と早期終了は別の事象です。通常のアサーションではループがチェックを続けることがありますが、エラーがスローされたり明示的にreturnしたりすると、後続の反復が実行されない場合があります。シナリオをケースに分けると、シナリオ間で制御フローを共有しなくなります。プロセスを終了させる失敗から復旧できるようになるわけではありません。

既存のユニットテストターゲットを準備する

この手順にはXcode 16以降を使用してください。Swift TestingはこれらのXcode配布版と、Swift 6.0以降のSwift.orgツールチェーンに含まれています。Swiftプロジェクトは通常のテスト作業には組み込み版を使うことを推奨しています。この例では外部パッケージへの依存は不要です。Swift Testingの入手を参照してください。

既存のユニットテストバンドルをそのまま使います。Appleは同じターゲット内でSwift TestingとXCTestを併用できることを明示しているため、新しいSwiftファイルを1つ追加するところから段階的に移行できます。XcodeでFile > New > New File From Templateを選び、Swift Testing Unit Testを選択して、そのファイルを既存のユニットテストターゲットに追加します。プロジェクトにテストターゲットがない場合は、File > New > TargetからUnit Testing Bundleを作成します。手順は Xcodeプロジェクトへのテスト追加に記載されています。

この手順では計算コードを直接テストします。デスクトップウィンドウの自動操作は行いません。Appleが文書化しているUI自動化の方法は引き続きXCTestを使います。このパラメーター化手法は、レイアウトを決める基盤ロジックに使用してください。同じテスト追加ガイドでフレームワークの境界が説明されています。

各シナリオを完全な引数レコードにする

Macアプリがタイルを行単位で配置するとします。列数が4の場合、タイルが0個なら行数は0、4個なら1行に収まり、5個なら2行必要です。具体的な目的は、これらのチェックを維持しながら、各シナリオを個別に確認して実行できるようにすることです。

シナリオ名、入力、期待される出力を含む1つのレコードを使います。これにより、ケースの追加や並べ替えをしても期待値が入力に結び付いたままになります。次の5つのレコードは例示用のテストデータであり、既存のアプリケーションから得られた観測結果ではありません。

シナリオ タイル数 列数 期待される行数 目的
empty 0 4 0 空の入力
one-tile 1 4 1 先頭行の一部
exact-row 4 4 1 ちょうど境界
wrap 5 4 2 境界を超える最初のタイル
two-full-rows 8 4 2 列数のちょうどの倍数

この完全に自己完結したファイルをユニットテストターゲットに配置します。アプリのモジュールや他のファイルがなくても試せるよう、合成した実装が含まれています。サービス、モック、ファイル、ネットワークアクセスは使用しません。

import Testing

private struct TileLayout {
    func rowCount(tileCount: Int, columns: Int) -> Int {
        precondition(tileCount >= 0)
        precondition(columns > 0)

        let fullRows = tileCount / columns
        let partialRow = tileCount % columns == 0 ? 0 : 1
        return fullRows + partialRow
    }
}

private struct LayoutScenario:
    Codable, Sendable, CustomTestStringConvertible {
    let name: String
    let tileCount: Int
    let columns: Int
    let expectedRows: Int

    var testDescription: String { name }

    static let examples: [LayoutScenario] = [
        .init(name: "empty", tileCount: 0,
              columns: 4, expectedRows: 0),
        .init(name: "one-tile", tileCount: 1,
              columns: 4, expectedRows: 1),
        .init(name: "exact-row", tileCount: 4,
              columns: 4, expectedRows: 1),
        .init(name: "wrap", tileCount: 5,
              columns: 4, expectedRows: 2),
        .init(name: "two-full-rows", tileCount: 8,
              columns: 4, expectedRows: 2)
    ]
}

struct TileLayoutTests {
    @Test("Tile rows", arguments: LayoutScenario.examples)
    fileprivate func rowCountMatchesScenario(
        _ scenario: LayoutScenario
    ) {
        let layout = TileLayout()
        let actualRows = layout.rowCount(
            tileCount: scenario.tileCount,
            columns: scenario.columns
        )
        #expect(actualRows == scenario.expectedRows)
    }
}

この移行では、反復を@Test(arguments:)に移します。各要素が1回の呼び出しの引数になります。Codableは選択的な再実行に必要なエンコード機能を提供し、CustomTestStringConvertibleは読みやすいシナリオラベルを提供します。Appleのパラメーター化テストの実装ではパラメーター化が、Swift Testingをさらに活用するではカスタム引数の説明が解説されています。

フィクスチャは意図的にテスト関数内で構築します。各呼び出しは独自のTileLayoutを作成し、前のシナリオに依存しません。既存のフィクスチャを移行する場合は、XCTestのフックが引き継がれると想定せず、セットアップを意図的に移してください。Swift TestingのスイートはXCTestCaseを継承しません。Appleはセットアップをイニシャライザーに、クラスまたはアクターのティアダウンをdeinitに対応付けて、XCTestからのテスト移行で説明しています。

この計算は、0以上のタイル数と、正の列数を受け付けます。プリコンディションがその契約を定義しており、この例のケースは無効な値を対象にしていません。実際のアプリケーションコードで無効な入力に対して復旧可能なエラーを返す必要がある場合は、契約を変更し、この表を拡張する前に対応するテストを追加してください。

個別の結果を確認し、失敗したケースを再実行する

宣言の横にあるひし形を使って、パラメーター化関数を実行します。次にTest navigatorを開き、関数を展開して各ケースを確認します。1つのケースを実行するには、そのケースのひし形にポインターを合わせ、再生コントロールをクリックします。親関数の再生コントロールをクリックすると、すべてのケースが実行されます。Appleのテストの実行と結果の解釈に両方の操作が記載されています。

報告動作を制御して確認するには、一時的にwrapレコードのexpectedRowsを2から3に変更します。パラメーター化関数全体を実行します。5つのケースが実行され、実装が2を返すため、wrapの比較が失敗するはずです。そのケースを選択して個別に再実行し、期待値を2に戻します。これは意図的なテストデータの変更であり、実装の不具合を示すものではありません。

失敗した期待値のResultsとArgumentsの詳細を確認し、計算値とシナリオ入力を比較します。全件実行については、View > Navigators > Reportsを開き、テストアクションを選択してTestsセクションを確認します。これらの確認用コントロールはAppleのテスト結果ガイドで説明されています。

選択実行には、読みやすいラベルに加えて別の要件があります。ランナーが各引数を決定的に照合できなければなりません。Appleの現行ドキュメントでは、CustomTestArgumentEncodable、特定のRawRepresentable型、Encodable、またはエンコード可能な識別子を持つIdentifiableが認められています。このサンプルのCodableレコードはエンコードの方法を満たします。カスタム表示文字列だけではこの要件を満たしません。パラメーター化テストの実装を参照してください。

5つのレコードすべての期待される計算結果は、Pythonの整数除算と剰余でローカルに確認しました。整数の切り捨て除算だけを使う誤った実装例では、one-tileとwrapが失敗します。この調査環境ではSwiftを利用できなかったため、Swiftファイルは確認しましたがコンパイルしていません。実行可能な検証手順は、上記のXcodeでの実行です。

ケース数とライフサイクルを意図どおりに保つ

有用な移行チェックでは、シナリオレコードの数、全件実行で実行されたケース数、選択したケースを単独で実行できるかの3点を比較します。この例で意図する件数は5です。件数が異なる場合は、計算を変更する前に引数の構築方法とテストの選択を確認してください。

対応する期待値は同じ場所にまとめます。別々の2つのコレクションをarguments:に渡すと、その直積が生成されます。入力が5件で期待値も5件なら、対応する5つのシナリオではなく25通りの組み合わせになります。すべての組み合わせに意味がある場合は別々のコレクションを使い、対応するデータにはレコードまたはzip()を使います。Appleはパラメーター化テストの実装でこの違いを説明しています。

zip()を使う場合は、ペアを構築する前にコレクションの長さを確認してください。Swiftは短い方のシーケンスで停止します。余分な期待値があっても、ケースが生成されないまま未使用になる可能性があります。単一のシナリオレコードを使えば、この種の不一致を避けられます。この切り詰め動作はSwiftのzip(_:_: )リファレンスに記載されています。

実行順序も移行の判断に影響します。Swift Testingは既定でケースを並列実行します。この例では引数が不変でフィクスチャもローカルなため、結果は順序に依存しません。一時的にシリアル実行が必要なパラメーター化関数には、@Test属性に.serializedを追加します。スイートにこのトレイトを適用すると、その中のテストとケースもシリアル化されますが、関係のないスイートはシリアル化されません。テストをシリアルまたは並列で実行するを参照してください。

シリアル実行しても状態が新しくなるわけでも、シナリオ間の依存関係が定義されるわけでもありません。あるケースが別のケースによる変更の後にしか成功しない場合は、そのケースを独立して実行できるように構成を見直してください。手順が意図的に1つの連続した処理を構成する場合は、1つのテスト内に保持し、処理全体をパラメーター化します。

この移行例では、アサーション全体でSwift Testingを使用します。XCTestアサーションを含むヘルパーを再利用する場合は、ツールチェーンの相互運用動作を確認してください。Appleの現行移行ガイドでは、ツールチェーンやパッケージのToolsバージョンによって異なるモードと既定値が説明されています。このファイルをすべてTestingに統一すれば、その依存を避けられます。XCTestからのテスト移行を参照してください。

移行を待つ必要がある場合、サポートされているXCTestの代替策は、シナリオごとに名前付きのパラメーターなしテストメソッドを1つ用意し、必要に応じて計算とアサーションのヘルパーを共有することです。Appleはテスト追加ガイドで、シナリオごとにテストを作ることを推奨しています。これならSwift Testingのパラメーター化を必要とせず、メソッドごとの個別の結果を保てます。

元の各シナリオに対応するケースが実行され、フィクスチャが意図した状態から始まり、選択したケースを単独で正常に実行できれば、変換は完了です。その変更の間も境界入力と期待値は維持してください。個別に報告されると、既存のチェックを見つけ、診断し、繰り返しやすくなります。

出典