共有する非同期フィクスチャの準備は、Swift Testingのスイートのinit() async throwsに記述します。Swift Testingはインスタンスのテストメソッドを呼び出す前にこのイニシャライザを完了するため、メソッドはすぐに使える状態を受け取ります。XCTestでは、代わりにsetUp() async throwsを使用します。

このガイドは、Xcodeでローカルのユニットテストデータを準備するMac開発者向けです。目的は、合成入力を使った決定論的な計算で、アサーションを開始する前に非同期の初期化を完了することです。AppleはテストをXCTestから移行するでイニシャライザを使う方法を説明しています。

Swift Testingはフィクスチャの初期化をどのように待つのですか?

インスタンスのテストメソッドでは、Swift Testingは包含するスイート型を初期化してから、そのインスタンス上でメソッドを呼び出します。スイートには引数なしで呼び出せるイニシャライザが必要です。このイニシャライザは非同期、throwing、またはその両方にできます。各インスタンスのテストメソッドは、それぞれ別のインスタンスで実行されます。これらはスイート型を使ったテスト関数の整理で説明されているライフサイクルのルールです。

その結果として生じる依存関係は明快です。フィクスチャの準備が戻り、イニシャライザが完了してから、テストメソッドが開始します。イニシャライザでは、準備処理を実際にawaitする必要があります。イニシャライザ内で処理を開始するだけでは、この依存関係は確立されません。

準備をどこに置くかは、次の判断表を参考にしてください。

状況 準備の場所 何によって準備完了が保証されるか
複数のインスタンステストで同じフィクスチャのレシピが必要 スイートのinit() async throws 各メソッドの前にawaitされたイニシャライザが完了する
フィクスチャが必要なのは1つのテストだけ async throwsのテスト本体 アサーション前の明示的なtry await
既存のテストがXCTestCaseを継承している setUp() async throws XCTestがメソッドの前に非同期セットアップを完了する
Swift Testingのテストがstaticまたはグローバル関数 テスト本体、または明示的に呼び出すヘルパー テストが準備を明示的にawaitする

この表は、フレームワークで文書化されているライフサイクルのルールをフィクスチャの配置に適用したものです。特に、スイートのイニシャライザはインスタンスごとの準備箇所です。スイート全体で一度だけ準備が行われるという意味ではありません。Appleのスイート初期化ルールとXCTestのセットアップ順序を参照してください。

Xcodeで非同期フィクスチャの完全な例を追加する

付属のSwift Testingに対応したXcode 16以降を使用します。Swiftプロジェクトのドキュメントによると、Swift TestingはSwift 6およびXcode 16に含まれており、追加のパッケージ依存関係は不要です。公式のSwift Testing READMEを参照してください。

既存のユニットテストターゲットで、File > New > New File From Templateを選択し、Swift Testing Unit Testを選びます。生成されたファイルの内容を、以下の完全な例に置き換えます。既存のXCTestユニットテストバンドルにSwift Testingのテストを含めることもできます。別のバンドルは不要です。この手順はAppleのXcodeプロジェクトへのテスト追加に沿っています。

import Testing

enum FixtureError: Error {
    case invalidInput
}

private actor SquareCalculator {
    let values: [Int]

    init(values: [Int]) {
        self.values = values
    }

    func squares() -> [Int] {
        values.map { $0 * $0 }
    }
}

struct CalculationFixture {
    let squares: [Int]
    let total: Int

    static func make(values: [Int]) async throws -> Self {
        guard !values.isEmpty,
              values.count <= 100,
              values.allSatisfy({ (0...100).contains($0) }) else {
            throw FixtureError.invalidInput
        }

        let calculator = SquareCalculator(values: values)
        let squares = await calculator.squares()

        return Self(
            squares: squares,
            total: squares.reduce(0, +)
        )
    }
}

@Suite
struct CalculationTests {
    let fixture: CalculationFixture

    init() async throws {
        fixture = try await CalculationFixture.make(values: [2, 4, 6])
    }

    @Test
    func squaresAreReady() {
        #expect(fixture.squares == [4, 16, 36])
    }

    @Test
    func totalIsReady() {
        #expect(fixture.total == 56)
    }
}

このサンプルでは、実際のローカルアクターを使って計算を行います。アクターの分離されたメソッドをアクターの外から呼び出すには、awaitが必要です。遅延、ランダムな入力、外部依存関係、適切なタイミングで完了する必要のあるバックグラウンド処理はありません。実行が実際に中断されるかどうかにかかわらず、出力は決定論的です。

フィクスチャファクトリーは、空の入力、100個を超える値、0...100の範囲外の値を拒否します。これらの例示的な上限により、算術処理を小規模に保てます。各値の二乗は最大10,000、合計は最大1,000,000です。無効な値は、黙って削除または置換されるのではなく、エラーになります。

指定された入力に対する期待値は2² + 4² + 6² = 4 + 16 + 36 = 56です。イニシャライザは、完了したフィクスチャを非オプショナルなletプロパティに格納します。そのため、テストメソッドで準備完了フラグやオプショナル値のアンラップは不要です。

非同期の前提処理はすでにイニシャライザで完了しているため、メソッド自体は同期のままにできます。テスト本体内に非同期処理を置く場合、Appleが文書化しているパターンは、その関数にasyncを指定し、処理を直接awaitすることです。非同期コードのテストを参照してください。

合成的な算術処理は、Pythonを使ってローカルで確認しました。この調査環境ではSwiftを利用できないため、Swiftの例は確認しましたが、ここではコンパイルも実行もしていません。上記の期待値はサンプルの計算を示すものであり、Xcodeでテストを実行した結果を報告するものではありません。

タスクを開始するだけではセットアップが完了しないのはなぜですか?

よくある間違いは、同期イニシャライザからTask { ... }を起動して、すぐに戻ることです。この場合、イニシャライザは処理の起動を終えていますが、テストメソッドの開始時点でも処理が実行中の可能性があります。

Swiftの言語ガイドでは、Task { ... }は構造化されていないタスクを作成すると説明されています。返されるハンドルを使えば、await handle.valueで結果を待てます。ハンドルを破棄すると、その明示的な待機箇所がなくなります。公式Swiftブックの構造化されていない並行処理のセクションを参照してください。

フィクスチャの初期化では、非同期イニシャライザ内でファクトリーを直接awaitするのが、依存関係を最も明確に示す方法です。既存のヘルパーが内部で処理を起動し、その完了前に戻る場合、ヘルパーをawaitしても戻り値を待つだけです。戻る時点でフィクスチャの準備が完了している契約に変更するか、呼び出し側がawaitできるタスク結果を公開してください。

固定時間のスリープを追加しても、依存関係は表現できません。時間が経過するのを待つだけなので、その時間内に準備が完了するかどうかに依存します。同様に、テストメソッドにasyncを指定しても、非同期呼び出しが可能になるだけです。依存する処理を引き続きawaitする必要があります。

XCTestでは同等のセットアップをどう行いますか?

XCTestには、専用の非同期セットアップ用フックがあります。各テストメソッドの前に実行される順序は、setUp() async throws、続いてsetUpWithError()、その後に同期のsetUp()です。テストメソッドはこれらのフックの後に実行されます。テスト状態のセットアップとティアダウンを参照してください。

同じフィクスチャをXCTestで使う場合を比較するには、最初の例と同じユニットテストターゲットに、次の完全な補助ファイルを追加します。XCTestをインポートし、先に定義したモジュールから参照可能なCalculationFixtureを使います。ヘルパー定義は最初のファイルに残し、重複させないでください。

import XCTest

final class CalculationXCTests: XCTestCase {
    private var fixture: CalculationFixture?

    override func setUp() async throws {
        try await super.setUp()
        fixture = try await CalculationFixture.make(values: [2, 4, 6])
    }

    func testTotalIsReady() throws {
        let readyFixture = try XCTUnwrap(fixture)
        XCTAssertEqual(readyFixture.squares, [4, 16, 36])
        XCTAssertEqual(readyFixture.total, 56)
    }
}

非同期の準備はセットアップで行うため、XCTestのメソッドも同期のままで構いません。XCTestのセットアップが独立したフェーズなので、オプショナルプロパティを使います。XCTUnwrapにより、フィクスチャがない場合は明示的な失敗になります。Appleは移行ガイドでこのアンラップ方法を説明しています。

このテストをSwift Testingに移行するときは、準備をスイートのイニシャライザに移し、import Testing、@Test、Swift Testingのexpectationを使います。通常のSwift Testingスイートでメソッドを単にsetUpと名付けても、文書化されたセットアップ方法にはなりません。Appleの移行手順では、セットアップ用のメソッド群を初期化に置き換えます。

初期化が早すぎるように見える場合に確認する点

まず、テストがインスタンスメソッドかどうか確認します。staticテストはスイートのインスタンスを必要としないため、自動的なインスタンス初期化に依存できません。グローバルテストにも包含するインスタンスはありません。どちらの場合も、テスト本体内でフィクスチャを構築してawaitします。サポートされているテスト形式はテストをXCTestから移行するで説明されています。

次に、準備処理の呼び出しチェーン全体を調べます。準備完了状態を実際に生成する処理を見つけ、各呼び出し元がそれを待つことを確認します。例では、ファクトリーが戻る前にアクターの計算が完了し、イニシャライザが完了する前にファクトリーが戻ります。アサーションに必要な処理がこの順序から外れることはありません。

最後に、セットアップのエラーを伝播させます。CalculationFixture.makeがthrowすると、スイートのインスタンスは初期化を完了できないため、そのインスタンス上でインスタンスのテストメソッドは実行できません。明示的にテスト対象の動作である場合を除き、try?とフォールバックを使って、準備の失敗を空のフィクスチャに置き換えるのは避けてください。この例では、無効な入力エラーによって、見かけ上は使えるフィクスチャが作成されないようにしています。

最後に、初期化の順序とテストのスケジューリングを区別してください。Swift Testingでは、既定でテストが並列実行されます。スイートの.serialized特性を使うと、そのスイート内のテストを直列実行できます。ただし、直列化してもawaitされていないタスクが待機済みの準備処理になるわけではなく、スイートのイニシャライザが一度だけ実行されるわけでもありません。Appleはテストの直列実行または並列実行で、この特性の適用範囲を説明しています。

このローカル計算では、フィクスチャを不変に保ち、各インスタンステスト用に個別に準備します。重要なのは依存関係の連鎖です。準備処理が完了済みのフィクスチャを返し、イニシャライザがその結果をawaitし、テストが初期化済みのプロパティを読み取ります。これにより、タイミングの仮定に頼らず、準備完了を具体的な条件で保証できます。

参考資料