XCTestからSwift Testingへの移行完全ガイド【2026年版】:@Test・パラメータ化テスト・Xcode 26新機能

Swift Testing(Xcode 16以降に同梱)とXCTestの違いから、@Testマクロ・パラメータ化テスト・Xcode 26のexit testsまで実例コードで解説。ヘルパー移行や相互運用モードの使い分けなど、実プロジェクトで段階的に移行する具体的な手順もまとめました。

Swift Testing移行ガイド 2026【XCTest対応】

最終更新: 2026年8月21日

Swift Testingは、Appleが2024年のWWDCで発表し、Swift 6・Xcode 16に同梱された新しいテストフレームワークで、XCTestを置き換えるためにゼロから設計されました。マクロベースの@Test#expectにより、40以上あったXCTAssert系関数を2つに集約し、並列実行・パラメータ化テスト・struct/actorベースのテスト分離を標準サポートします。この記事では、XCTestからSwift Testingへ段階的に移行する具体的な手順を、Xcode 26の新機能(exit tests・test scoping traits・相互運用モード)まで含めて解説します。

  • Swift TestingはWWDC 2024で発表され、Swift 6・Xcode 16以降のツールチェーンに同梱されている(XCTest自体は非推奨ではない)。
  • @Testマクロで任意の関数をテスト化でき、XCTestCase継承やtestプレフィックスは不要になった。
  • アサーションは#expect#requireの2つのマクロに集約され、Swiftの通常の演算子で条件を書ける。
  • WWDC 2026で追加された相互運用モード(Limited/Complete/Strict)により、XCTestとSwift Testingを段階的に共存させられる。
  • Xcode 26ではexit teststest scoping traitsが追加され、preconditionfatalErrorのテストが可能になった。
  • UIテスト(XCUITest)とパフォーマンス測定(XCTMetric)は今もXCTestのみに残る。

Swift Testingとは何か?XCTestとの根本的な違い

Swift Testingは、AppleがWWDC 2024で発表し、オープンソースとしてGitHubで公開されているモダンなテストフレームワークです。Objective-CのSenTestingKitの系譜を受け継ぐXCTestとは異なり、Swiftの言語機能(マクロ、値型、構造化並行処理、マクロ展開時のコンパイル時検証)を前提に設計されています。XCTestが実行時にtestプレフィックスを頼りにテストを発見していたのに対し、Swift Testingではマクロがビルド時にテストを登録するため、命名規約に縛られません。

もう一つの根本的な違いは実行モデルです。XCTestは1つのXCTestCaseインスタンスを共有しつつメソッドを直列に呼び出しますが、Swift Testingはテストごとに新しいインスタンス(多くの場合struct)を作成し、既定で並列実行します。私自身、Objective-C時代からテストを書き続けてきましたが、Swift Testingへ移行してから、共有状態に依存した「たまに落ちるテスト」がほぼ根絶されました。並列実行が既定なので、隠れていた共有ミュータブル状態が一気に浮き彫りになるからです。

API面ではXCTestのXCTAssertEqualXCTAssertTrueXCTAssertThrowsErrorなど40以上のアサーション関数が、#expect#requireの2つのマクロに集約されました。マクロは失敗時に式ツリーを展開して差分を表示するため、XCTAssertEqual(a, b)より#expect(a == b)のほうがはるかに読みやすい失敗メッセージを吐きます。SwiftのAPI設計として素直で、私は正直、もっと早くこうなってほしかったと思います。

@Testマクロと#expect:最小コードで書く新しいテスト

Swift Testingで最も基本となるのが@Testマクロです。任意の関数に付けるだけでテストとして登録され、XCTestCaseを継承する必要はありません。関数名も自由で、testで始める必要すらありません。

import Testing
@testable import MyApp

@Test func 加算が正しく動作する() {
    let calculator = Calculator()
    #expect(calculator.add(2, 3) == 5)
}

@Test("マイナス値の加算")
func 負の値の加算() {
    #expect(Calculator().add(-1, -2) == -3)
}

2番目のテストのように、@Test("...")に文字列を渡すと、それがXcodeのテストナビゲータに表示される人間可読な名前になります。日本語プロジェクトでは、関数名を英数字にしつつ表示名だけ日本語にする使い方が現実的です。

アサーションには2つのマクロがあります。#expectは失敗しても後続を実行しますが、#requireは失敗するとテストを打ち切ります(tryで呼ぶ必要があります)。オプショナルのアンラップにも使えます。

@Test func ユーザー取得() async throws {
    let user = try #require(await UserRepository().fetch(id: 1))
    #expect(user.name == "Lukas")
    #expect(user.age >= 0)
}

例外テストもXCTestのXCTAssertThrowsErrorより直感的です。#expect(throws:)にエラーの型(またはインスタンス)を渡します。

@Test func 空文字はエラーを投げる() {
    #expect(throws: ValidationError.empty) {
        try Validator.check("")
    }
}

@Suiteでテストを整理し、init/deinitでセットアップする

複数のテストをグループ化するには@Suiteを使います。型はstructenumactorfinal classのいずれでも構いません。個人的にはデフォルトでstructを推奨します。値型なので、テストごとに新しいインスタンスが作られる際のコストが小さく、共有状態を持ちにくいからです。

@Suite("ショッピングカートのテスト")
struct CartTests {
    let cart: Cart

    init() {
        cart = Cart()
        cart.add(Item(name: "本", price: 1500))
    }

    @Test func 合計金額が正しい() {
        #expect(cart.total == 1500)
    }

    @Test func 削除で空になる() {
        cart.remove(at: 0)
        #expect(cart.isEmpty)
    }
}

XCTestのsetUp()tearDown()に相当する処理は、それぞれinitdeinitで書きます。テストごとに新しいインスタンスが生成されるため、varプロパティを共有する必要はなく、テスト間の状態リークが起きません。これはXCTestで最もはまりやすい落とし穴の1つを構造的に排除しています。

非同期セットアップが必要な場合は、initasync throwsにできます。throwsにすればセットアップ失敗時に該当テストだけがスキップされます。Swift 6.2の並行処理の変更点と組み合わせると、@MainActor@concurrent属性をSuite全体に適用してUIコードのテストを安全に書けます。

パラメータ化テストで重複コードを一掃する

XCTestで最も要望が多かった機能の1つがパラメータ化テストでした。XCTestではforループで自前実装するしかなく、失敗時にどのケースで落ちたか分かりにくいという問題がありました。Swift Testingでは@Testarguments:を渡すだけで、各引数が独立したテストケースとして実行されます。

@Test("メールアドレスのバリデーション",
      arguments: [
          "[email protected]",
          "[email protected]",
          "[email protected]"
      ])
func 有効なメールを受け入れる(email: String) {
    #expect(EmailValidator.isValid(email))
}

Xcodeのテストナビゲータでは、各引数が個別の子ノードとして表示され、ワンクリックで単一ケースだけ再実行できます。私が実務で最初に感動したのはここでした。失敗ケースだけを繰り返し検証できるのは、TDDの体験を根本的に改善します。

複数の引数を組み合わせたい場合はzipやタプルの配列を渡します。デカルト積が必要なら、arguments:を2回渡すだけで全組み合わせが自動生成されます。

@Test(arguments: [1, 2, 3], ["A", "B"])
func 全組み合わせを検証(number: Int, letter: String) {
    // 3 × 2 = 6ケースが自動生成される
    #expect(!"\(letter)\(number)".isEmpty)
}

Traitsでテストを制御する:.tags・.serialized・.disabled

Traits@Test@Suiteの挙動を変更する追加パラメータで、これがSwift Testingの拡張性の要です。既定でテストは並列実行されますが、DBやファイルシステムに触れるテストは.serializedを付けて直列化します。

@Suite("データベース統合テスト", .serialized)
struct DatabaseTests {
    @Test func レコード挿入() async throws { /* ... */ }
    @Test func レコード削除() async throws { /* ... */ }
}

.tagsは横断的な分類に便利です。まずタグを1箇所で定義して再利用します。

extension Tag {
    @Tag static var network: Self
    @Tag static var slow: Self
}

@Test(.tags(.network, .slow))
func APIレスポンスのテスト() async throws { /* ... */ }

Xcodeのテストナビゲータ(⌘-6)でタグアイコンをクリックすると、ファイル構造ではなくタグ別にテストを一覧できます。CIでは--filterと組み合わせてnetworkタグだけ除外実行、といった運用が可能になります。

一時的にテストを無効化するには.disabled("理由")を使います。理由を必須にする設計はよく考えられていて、TODOコメントが腐りにくくなります。関連Issueがある場合は.bug("https://github.com/org/repo/issues/42", "説明")で紐付けると、失敗レポートにリンクが表示されます。

Xcode 26の新機能:exit testsとtest scoping traits

Xcode 26(Swift 6.2)で追加されたexit testsは、Swift Testingの長年の弱点だった「クラッシュを検証するテスト」を可能にしました。preconditionfatalErrorexit()で終了する処理を、テストから安全に検証できます。内部的にはサブプロセスを起動して隔離実行されるため、テストランナー自体は落ちません。

@Test func 不正入力でクラッシュする() async {
    await #expect(processExitsWith: .failure) {
        precondition(false, "この分岐は到達不能のはず")
    }
}

@Test func 特定の終了コードで終わる() async {
    await #expect(processExitsWith: .exitCode(EX_USAGE)) {
        exit(EX_USAGE)
    }
}

プロセスの標準出力・標準エラー出力もキャプチャできます。#require(processExitsWith:observing:)にキーパスを渡すと、必要な出力だけがメモリに読み戻されます。無関係な出力までコピーしない設計はメモリ効率上ありがたい選択です。

もう一つの新機能がtest scoping traitsです。カスタムトレイトを実装して、テスト前後にコードを実行できます。データベーストランザクションのロールバック、環境変数のセット・アンセットといった横断的関心事を、テスト本体を汚さずに宣言的に注入できます。WWDC 2026「Migrate to Swift Testing」セッションで詳細が解説されています。

加えてXcode 26ではattachmentsもサポートされ、テスト失敗時のスクリーンショットや診断ログをAttachable準拠の型として結果に添付できます。CI上でも成果物として保存されるため、フレーキーなテストの原因調査が劇的に楽になります。

XCTestからSwift Testingへの段階的な移行手順

既存のXCTestベースの大規模プロジェクトを一気に書き換える必要はありません。Appleも公式に段階的移行を推奨しています。以下は実プロジェクトで私が採用している手順です。

  1. 既存のXCTestはそのまま残す。 ビルドが通っている限り、触らないほうが安全です。
  2. 新規テストはすべてSwift Testingで書く。 同じテストターゲット内に共存できるため、追加設定は不要です。import Testingimport XCTestを同一ファイルに書いても構いません(ただし1つのテスト関数内で混在させないこと)。
  3. 共通ヘルパーから移行を始める。 テストヘルパー内のXCTFail("...")Issue.record("...")に、XCTAssertEqual#expectに置き換えます。#file#lineを受けているヘルパーは、SourceLocationパラメータに更新します。
  4. パラメータ化候補を探す。 テスト関数内にforループがあり、要素ごとにアサートしている箇所は、ほぼ確実にパラメータ化テストへ移行すべきです。
  5. precondition/fatalErrorを持つコードにexit testsを追加。 これまでテスト不能だった分岐が、Swift Testingで初めて検証可能になります。
  6. 最後にレガシーなXCTestCaseをSuiteに置き換える。 このタイミングでclassstructに変えると、共有状態バグが露見しやすくなります。

UIテスト(XCUITest)とパフォーマンス測定(XCTMetric)はSwift Testingに移行できません。Appleが代替を出すまではXCTestに残します。@ObservableへのSwiftUI状態管理の移行と同様、ゆっくりでいいので確実に、というのが私の一貫した推奨です。

相互運用モード:Limited・Complete・Strictの使い分け

WWDC 2026で導入された相互運用モード(Interoperability Mode)は、XCTestとSwift Testingの境界を跨いだAPI呼び出しをどこまで許可するかを制御します。Xcodeのテストプランで設定でき、3段階から選べます。

モードクロスフレームワーク呼び出し推奨用途
Limited警告扱い(テストは合格)移行を始めたばかりのプロジェクト。Xcode 27以前のテストプランの既定。
Completeエラー扱い(テストが失敗)ヘルパーの移行を計画的に進めている中盤フェーズ。
Strictビルド時に検出・完全禁止移行が完了したプロジェクト、または新規プロジェクト。

個人的な運用としては、新規プロジェクトはStrictで開始し、既存プロジェクトはLimitedから始めて、ヘルパー移行が終わった段階でCompleteに引き上げます。Strictは全ヘルパーがSwift Testingネイティブになってから、というのが失敗の少ないルートです。

相互運用の恩恵は、既存のXCTestCaseヘルパー資産を捨てずに済むことです。たとえば長年育ててきたスナップショットテスト用のXCTAssertSnapshotラッパーがある場合、Limitedモードなら新しい@Test関数内からそのまま呼び出せます。移行期間中に「全ヘルパーを書き直すまでSwift Testingが使えない」という状況を避けられるのは大きな利点です。

よくある質問

Swift Testingを使うにはXcodeのバージョンはいくつ必要ですか?

Swift TestingはXcode 16以降(Swift 6ツールチェーン)に同梱されています。exit testsやtest scoping traitsといった新機能を使うにはXcode 26(Swift 6.2)が必要です。オープンソース版としてLinuxやWindowsでも利用できます。

XCTestは非推奨になりましたか?

いいえ、XCTestは2026年8月時点で非推奨ではありません。急いで移行する必要はなく、UIテスト(XCUITest)とパフォーマンス測定(XCTMetric)は今もXCTestでしか書けません。Appleは新規テストからSwift Testingを採用し、既存テストは段階的に移行することを推奨しています。

@Testと@Suiteの違いは何ですか?

@Testは個別のテスト関数に付けるマクロで、@Suiteはテストをグループ化する型(struct・enum・actor・final class)に付けるマクロです。@Suiteを明示しなくても、内部に@Test関数を持つ型は自動的にSuiteとして扱われますが、表示名や共通のTraitsを指定したい場合は@Suite("名前", .serialized)のように明示します。

XCTestとSwift Testingは同じプロジェクトで共存できますか?

はい、同じテストターゲット・同じファイル内でも共存可能です。ただし1つのテスト関数の中で混在させることは避けてください(Swift Testing側でXCTAssertを呼ぶ、あるいはその逆)。WWDC 2026で導入された相互運用モードを使うと、境界越しの呼び出しをLimited/Complete/Strictの3段階で制御できます。

Swift Testingでパラメータ化テストはどう書きますか?

@Test(arguments: [値1, 値2, 値3])のように、argumentsラベル付きで配列を渡し、テスト関数側で対応する引数を受け取ります。各値ごとに独立したテストケースとして実行され、Xcodeのテストナビゲータで個別に再実行できます。複数の引数配列を渡すとデカルト積が自動生成されます。

setUp()とtearDown()はSwift Testingで何に置き換わりますか?

それぞれSuite型のinitdeinitに置き換わります。テストごとに新しいインスタンスが生成されるため、XCTestのsetUp()より安全で、テスト間の状態リークが起こりません。非同期セットアップが必要な場合はinitasync throwsで宣言できます。

Lukas Müller
著者について Lukas Müller

iOS developer and Swift author since the Objective-C days. Spends his evenings on side projects and his mornings on SwiftUI internals.