Swift Testing:XCTestを置き換えるフレームワーク、そしてXCTestに残るもの
2013年以来Appleのテストフレームワークとして使われてきたXCTestは、Swift Testingに置き換えられつつあります。Appleはまだ「非推奨」とは明言していません(XCTestは現在も提供されており、動作し、UI自動化テストやパフォーマンステストを担っています)が、WWDC 2024以降、Appleの全セッション、Swift Forumのスレッド、新しいサンプルプロジェクトはすべてSwiftコードをSwift Testingへと向けています。このフレームワークはXcode 16+およびSwift 6で提供され1、macOS、iOS、watchOS、tvOS、visionOS、Linuxで動作し2、異なるメンタルモデルを提示します。テストは関数、スイートは型、設定はトレイト、そして並列実行がデフォルトです。
移行は段階的に行えます。Apple自身のドキュメントでは、同一ターゲット内で両フレームワークを並行して動作させることを明示的にサポートしており3、これはつまり、規模のあるコードベースにとって正しい選択は「すべてを書き直す」ではなく、「新しいテストはSwift Testingで書き、古いテストは触れる際に移行する」だということです。本稿ではAPI表面を整理し、移行境界(XCTestに残るもの)を明確にし、XCTestのクラス階層による設定を置き換えるトレイトの語彙を取り上げます。
TL;DR
- Swift TestingはXCTestのクラスベース・文字列型のモデルをマクロで置き換えます:
@Test、@Suite、#expect(...)、#require(...)4。 - テストはフリー関数または型のメソッドとして書きます。スイートはテスト関数を含む任意の型であり、
@Suiteは表示名やトレイトを指定する場合にのみ必須となります。 - 設定はクラス階層や命名規則からトレイトへ移行します:
.enabled(if:)、.disabled(_:)、.serialized、.timeLimit(...)、.tags(...)、.bug(...)5。 - パラメータ化テストは
@Test(arguments:)によって、引数ごとに子テスト結果を持つ1つの親テスト結果を生成します。デフォルトで並列実行されます6。 - 移行は一括ではなく並行型です。UI自動化(
XCUIApplication)とパフォーマンステスト(XCTMetric)はSwift Testingでカバーされていないため、XCTestに残ります3。 - WWDC 2025ではカスタムアタッチメント(テスト成果物)と終了テスト(特定の条件下で終了することが期待されるコードの検証)が追加されました7。
なぜSwift Testingが存在するのか
XCTestの問題点は、大規模なiOSテストスイートを保守してきた人ならよく知っているはずです。
文字列型のアサーション。 XCTAssertEqual(a, b, "message")は元のSwift式を捕捉しません。アサーションが失敗したとき、失敗メッセージはランタイム値を伝えますが、どの式がそれを生んだかは伝えません。デバッグするにはテストソースを読み返してコンテキストを再構築する必要があります。
クラスベースの設定。 セットアップ、ティアダウン、共有状態にはXCTestCaseのサブクラス化が必要です。テストを無効化するには、リネームする(発見パターンに引っかからなくする)か、#if DEBUGで囲むかのいずれかが必要となります。どのテストを実行するかの選択には慎重な命名が要求されます。
並行性のミスマッチ。 XCTestはSwift Concurrencyより前のものです。XCTestExpectationとwait(for:timeout:)は非同期コードへのレガシーブリッジであり、このパターンは冗長でエラーが出やすいものです。モダンなSwiftコードはasync/awaitを使いますが、XCTestのイディオムは時代遅れに感じられます。
弱いパラメータ化。 XCTestはパラメータ化テストを第一級でサポートしていません。開発者はテストメソッド内のforループでそれを偽装する(多くのケースに対して1つのテスト結果しか得られない)か、コードジェネレータを書く(テンプレートからN個のテストメソッドを生成する)ことになります。
Swift Testingはそれぞれに対処します。
import Testing
@Test func userInitializesWithDefaults() {
let user = User()
#expect(user.name == "Anonymous")
#expect(user.preferences.isEmpty)
}
#expectは式全体を捕捉します。アサーションが失敗すると、テスト出力にはuser.name == "Anonymous"が失敗した理由としてuser.nameが"Guest"であったことが示されます。マクロが構文木を見るため、フレームワークが式を読み取る作業を担うのです。「期待値X、実際値Y」の推測はもう不要です。
マクロ
4つのマクロが仕事を担います。
@Testと@Suite
@Testは関数をテストとしてマークします。関数はファイルスコープ、構造体、actor、enum、Swiftで関数が存在できる場所ならどこにでも置けます。フレームワークは@Testでマークされた関数を発見して実行します。
@Test func loginSucceedsWithValidCredentials() async throws {
let result = try await api.login(username: "alice", password: "valid")
#expect(result.isAuthenticated)
}
struct AuthenticationTests {
@Test func loginRejectsEmptyPassword() async throws {
let result = try await api.login(username: "alice", password: "")
#expect(!result.isAuthenticated)
#expect(result.error == .emptyPassword)
}
}
@Suiteは型をテストスイートとして明示的にマークします。型が@Test関数を含んでいる場合、このマーカーは暗黙的です。明示的な@Suiteはカスタム表示名を追加するときや、スイートレベルでトレイトを適用するときにのみ必要となります4。
@Suite("Authentication", .tags(.security))
struct AuthenticationTests {
@Test func loginSucceeds() async throws { ... }
@Test func loginFails() async throws { ... }
}
#expectと#require
#expectは標準のアサーションです。期待が失敗してもテストは続行します(失敗は記録されますが、後続のアサーションも実行されます)。#requireはフェイルファスト版です。要件が満たされなかった場合、テストは即座に停止します。#requireは、その失敗が後続のアサーションで意味のないエラーを引き起こすような前提条件に使いましょう。
@Test func userPreferencesLoad() async throws {
let user = try #require(await store.fetchUser(id: 42))
// fetchUserがnilを返した場合、テストはここで停止する。
// 次の行はアンラップされたオプショナルに対して実行されない。
#expect(user.preferences["theme"] == "dark")
#expect(user.preferences.count >= 1)
}
#requireの前のtryは重要です。#requireは要件が満たされた場合、アンラップされた値(この場合は非オプショナルのUser)を返し、満たされなかった場合はthrowします。アンラップに#requireを使うには、テスト関数がthrowsである必要があります。
パラメータ化テスト
@Test(arguments:)はテスト関数を引数ごとに1回ずつ実行します。フレームワークは各引数を子テストとして報告し、親テストはそのロールアップとなります。
@Test(arguments: [
("alice", "password123", true),
("bob", "", false),
("", "password123", false),
("carol", "wrongpassword", false),
])
func loginScenario(username: String, password: String, expectedSuccess: Bool) async throws {
let result = try await api.login(username: username, password: password)
#expect(result.isAuthenticated == expectedSuccess)
}
フレームワークは4つの引数をデフォルトで並列に実行します。テストの失敗メッセージはどの引数タプルが失敗したかを特定するため、パラメータ化された失敗のデバッグでテストを単独で実行する必要はありません。
引数が独立したコレクションの場合、@Test(arguments: arrayA, arrayB)は直積(すべての組み合わせ)を実行します。順序のあるペアの場合はzip(arrayA, arrayB)を使い、結果のシーケンスを渡します。
トレイト:第一級の値としての設定
XCTestはテストの設定にクラス階層、命名規則、Xcodeテストプランを使います。Swift Testingはトレイトを使います。トレイトとは@Testや@Suiteの宣言に適用される値です5。
@Test(.enabled(if: ProcessInfo.processInfo.isRunningOnCI))
func ciOnlyIntegrationTest() async throws { ... }
@Test(.disabled("Pending fix for FB12345"))
func knownFailingTest() { ... }
@Test(.timeLimit(.minutes(1)))
func potentiallySlowTest() async throws { ... }
@Test(.tags(.network, .integration), .bug("FB67890", "regression in retry logic"))
func networkRetryTest() async throws { ... }
@Suite(.serialized)
struct DatabaseTests {
// このスイート内のすべてのテストは順次実行される。
// テストがデータベースのフィクスチャを共有するときに有用。
@Test func insertUser() { ... }
@Test func updateUser() { ... }
@Test func deleteUser() { ... }
}
トレイトの語彙はXCTestがカスタムインフラを必要としていたケースをカバーします。
.enabled(if:)と.disabled(_:)。 実行時の式とオプションの理由による条件付き有効化です。#if DEBUGブロックやリネームのトリックを置き換えます。.serialized。 スイートまたはパラメータ化テストを順次実行とマークします。継承可能で、シリアライズされたスイートはすべての子テストを順次実行に強制します6。.timeLimit(...)。 テストごとの時間制限です。テストが制限を超えると強制終了され、失敗として報告されます。.tags(...)。 フィルタリング用のタグです。Xcodeテストプランで含める/除外するタグルールを設定します。プランはxcodebuild test -testPlan <name>経由で実行します。なお-only-testing:はテスト識別子(ターゲット、スイート、関数)でフィルタリングし、タグではフィルタリングしません。.bug(...)。 テストをバグIDと理由にリンクします。リンクはテストレポートに表示され、(既知の失敗テストでは)失敗がCIをブロックするのを防ぎます。
カスタムトレイトもサポートされています。アプリは独自のトレイトをプロジェクト固有のニーズに合わせて定義できます(例:.requiresKeychain、.skipIfOffline)。
並列実行がデフォルト
Swift Testingはパラメータ化された引数を含め、テストをデフォルトで並列実行します。デフォルトであることが重要なのは、ほとんどのテストスイートが時間とともに大きくなり、並列実行が30秒のテスト実行と5分のテスト実行を分けるからです。XCTestは明示的なオプトイン(テストプランの並列実行トグル)を必要としていましたが、Swift Testingはデフォルトを反転させました。
その含意として、テストは独立していなければなりません。共有状態(データベースフィクスチャ、モックされたファイルシステム、アプリレベルのシングルトン)はテストごとに分離する(各テストが新しいフィクスチャを得る)か、.serializedでマークする(スイートが順次実行される)必要があります。グローバル状態を変更し順次実行を仮定するテストはバグであり、Swift TestingはそれをXCTestよりも早く露呈させます。
移行:一括ではなく並行型
Appleの移行に関する公式の立場は段階的なものです3。
- 両フレームワークは同じテストターゲット内に共存します。
- 新しいテストはすぐにSwift Testingで書けます。
- 古いXCTestテストは変更なしで動作し続けます。
- XCTestテストは都合の良いタイミング(触れたファイル、新機能、リファクタ)でSwift Testingに移行します。
2つのケースは無期限にXCTestに残ります。
UI自動化テスト。 XCUIApplication、XCUIElement、XCUIElementQuery、UIテストのAPI全体はXCTest専用です3。Swift Testingは代替を提供していません。AppleがSwift Testingのカバレッジを拡張するまで、UIテストはXCTestに残ります。
パフォーマンステスト。 XCTMetric、measure(metrics:)、パフォーマンスベースラインもXCTest専用です3。Swift Testingにはまだ同等のものはありません。
それ以外(ユニットテスト、統合テスト、非同期テスト、パラメータ化テスト)については、Swift Testingが新規コードの推奨デフォルトとなります。
WWDC 2025で追加されたもの
WWDC 2025でSwift Testingに2つの注目すべき追加がありました7。
カスタムアタッチメント。 テストは失敗のトリアージのために、結果に任意のAttachableデータをアタッチできます。失敗時のスクリーンショット画像、診断ログ、生成された入力ファイルなどです。アタッチメントはXcodeのテストレポーターやCIのアーティファクトに表示されます。Sources/Testing/Attachments/Attachment.swift内のAttachment.record(_:named:sourceLocation:)APIがその表面を定義し、Attachableに準拠する値(Data、String、オプトインの準拠による画像型)が受け入れられます。
@Test func reportLayoutAtFailure() async throws {
let result = await renderer.run(LayoutTestCase.complex)
if !result.passes {
Attachment.record(result.diagnostics, named: "diagnostics.txt")
Attachment.record(result.screenshotData, named: "failure-screenshot.png")
}
#expect(result.passes)
}
終了テスト。 終了することが期待されるコード(exit()の呼び出し、致命的エラーのthrow、precondition failureのトリガー)を検証するテストです。XCTestにはこれらをテストするきれいな方法がありませんでした。Swift Testingの終了テストは子プロセス内でコードを実行し、終了の挙動を検証します。
@Test func parserExitsOnMalformedInput() async {
await #expect(processExitsWith: .failure) {
Parser.parse(malformedInput)
}
}
両方とも実用的な追加です。カスタムアタッチメントは現実の痛点(不安定なCI失敗のデバッグ)を解決し、終了テストはユニットテスト表面における本物のギャップをカバーします。
XCTestが依然として正解の場面
新規コードでもXCTestを使うべき3つのケースがあります。
テストがUI自動化テストである場合。 XCUIApplication.launch、タップ&スワイプの操作、accessibility-identifier-basedクエリ。Swift TestingはUI自動化表面をカバーしていないため、新しいUIテストはXCTestとなります。
テストがパフォーマンスのベンチマークである場合。 measure(metrics: [XCTClockMetric()])、パフォーマンスベースライン、メモリ計測。Swift Testingには同等のものがないため、新しいパフォーマンステストはXCTestとなります。
テストがSwift Testingをサポートしないプラットフォームで動作する必要がある場合。 Swift TestingはSwift 6 / Xcode 16+を要求します。それより古いツールチェーンに対してコンパイルしなければならないテスト(古いXcodeバージョンを含むCIマトリックス)は、ツールチェーンが追いつくまでXCTestを必要とします。
Swift 6+ターゲットでのユニットテスト、統合テスト、非同期テスト、パラメータ化テストには、Swift Testingがモダンなデフォルトです。移行コストは現実のものですが、移行しないコストは複利的に膨らみます。今日書かれる新しいXCTestはすべて技術的負債なのです。
エージェントワークフローとの接続
テストはAI支援によるコード生成が動作する契約です。エージェントがSwiftコードを書くとき、開発者(あるいはエージェント自身)が変更が正しいかを検証する手段がテストです。Swift Testingの特性はエージェント生成コードをより検証可能にします。
- 式の捕捉。
#expect(user.score == calculator.compute(user))の失敗は比較の両側を表示します。失敗を読むエージェントはテストファイルを読み返さずにバグを修正できます。 - パラメータ化されたケース。 リグレッションを修正するエージェントは、どの特定の入力が失敗したかを知ることができます。修正は的を絞ったものになります。
- トレイトベースのフィルタリング。
.tags(.regression)アノテーションがあれば、エージェントは修正後にリグレッションテストだけを実行できます。フィードバックループが速くなります。 - カスタムアタッチメント。 不安定なテストをデバッグするエージェントは、失敗した入力アーティファクトをアタッチして、次の実行で読み取れます。
Apple開発のためのフックに関する記事では、すべてのClaude Code編集後にテストを実行するStopフックを取り上げています。Swift Testingのより速く、より並列で、より情報豊富な失敗は、そのループをエージェントが手動介入なしに反復できるほどタイトにしてくれます。
このパターンがiOS 26+アプリに意味するもの
3つの要点です。
-
新規テストはSwift Testingをデフォルトにしましょう。 このフレームワークはより表現豊かで、デフォルトで並列実行され、失敗コンテキストを自動的に捕捉し、パラメータ化テストを第一級の概念としてサポートします。既存のXCTestコードは動作し続け、新しいコードはモダンなデフォルトを使います。
-
UI自動化とパフォーマンステストはXCTestに残しましょう。 Appleのカバレッジのギャップであり、移行の選択ではありません。Swift Testingがそれらの領域に拡張されるまで、それらのテストはXCTestに無期限に残ります。
-
設定の語彙としてトレイトを使いましょう。
.enabled(if:)、.disabled(_:)、.serialized、.timeLimit(...)、.tags(...)、.bug(...)は、XCTestがクラス階層、命名規則、#if DEBUGブロックを必要としていたケースをカバーします。トレイトは合成可能な第一級の値であり、トレイトの語彙こそがフレームワークをモダンに感じさせるものなのです。
Apple Ecosystemクラスター全体:型付きApp Intents、MCPサーバー、ルーティングの問題、Foundation Models、ランタイム対ツーリングLLMの区別、3つの表面、単一の真実の源パターン、2つのMCPサーバー、Apple開発のためのフック、Live Activities、watchOSランタイム、SwiftUIの内部、RealityKitの空間メンタルモデル、SwiftDataのスキーマ規律、Liquid Glassパターン、マルチプラットフォーム出荷、プラットフォームマトリックス、Visionフレームワーク、シンボルエフェクト、Core MLの推論、Writing Tools API、書かないと決めたこと。ハブはApple Ecosystem Seriesにあります。AIエージェントを使ったiOS開発のより広いコンテキストについてはiOS Agent Developmentガイドをご覧ください。
FAQ
Swift TestingはXCTestを完全に置き換えますか?
まだです。XCTestは依然としてUI自動化テスト(XCUIApplication)とパフォーマンステスト(XCTMetric)をホストしており、Swift Testingはそれらの領域をカバーしていません。ユニットテスト、統合テスト、非同期テスト、パラメータ化テストについては、Swift Testingが新規コードの推奨デフォルトです。両フレームワークは衝突なしに同じターゲット内に共存します。
#expectとXCTAssertEqualの実用的な違いは何ですか?
#expectは完全なSwift式を捕捉し、失敗時に比較の両側を報告します(例:user.score == calculator.compute(user)が失敗したのはuser.score = 80かつcalculator.compute(user) = 100であったため)。XCTAssertEqualはランタイム値とオプションのメッセージ文字列しか知りません。式の捕捉こそが、Swift Testingの失敗をテストソースを読み返さずに自己説明的にしてくれるものです。
パラメータ化テストはどのように動作しますか?
@Test(arguments: [...])はテスト関数を引数ごとに1回ずつ実行します。フレームワークは各引数を子テスト結果として報告し、親をロールアップとします。タプルは複数の値を渡せます。パラメータ化テストはデフォルトで並列実行されます。順次実行に強制するには.serializedを追加しましょう。
#requireは何のためにありますか?
#requireは#expectのフェイルファスト版です。その失敗が後続のアサーションで意味のないエラーを引き起こすような前提条件に使いましょう。try #require(value)は満たされたときにアンラップされた値を返し、満たされなかったときにthrowするので、テストの残りはアンラップされた値を直接使えます。アンラップに#requireを使うには、テスト関数がthrowsである必要があります。
Swift TestingのテストをLinuxで書けますか?
はい。Swift Testingはオープンソース2で、Linuxを含むSwiftがサポートするすべてのプラットフォームで動作します。サーバーサイドのSwiftプロジェクトはSwift Testingを直ちに採用できます。このフレームワークはAppleプラットフォーム専用ではありません。
既存のXCTestスイートはどう移行すればよいですか?
Appleの推奨は段階的です。新しいテストはSwift Testingで書き、古いものは触れる際に移行しましょう3。自動コンバーターはありません(モデルの差が大きすぎて自動化は脆くなります)。適切な移行対象はユニットテストと統合テストです。UIテストとパフォーマンステストはXCTestに無期限に残ります。
参考文献
-
Apple Developer:Swift Testing。Xcode 16+およびSwift 6でのSwift Testing採用に関するAppleの概要。 ↩
-
オープンソースリポジトリ:swiftlang/swift-testing on GitHub。フレームワークのソースコードで、Swift Projectの標準ライセンスとともにApache License 2.0で利用可能です。 ↩↩
-
Apple Developer Documentation:Migrating a test from XCTest。並行共存とXCUIApplication / XCTMetricの例外をカバーするAppleの公式移行ガイド。 ↩↩↩↩↩↩
-
Apple Developer Documentation:Swift Testing。
@Test、@Suite、#expect、#require、トレイトの語彙のフレームワークリファレンス。 ↩↩ -
Apple Developer Documentation:Trait。テストトレイトを定義するプロトコルで、
.enabled(if:)、.disabled(_:)、.serialized、.timeLimit(...)、.tags(...)、.bug(...)を含むビルトイントレイトがあります。 ↩↩ -
Apple Developer:Meet Swift Testing(WWDC 2024セッション10179)とGo further with Swift Testing(WWDC 2024セッション10195)。パラメータ化テストと並列実行をカバーする紹介セッション。 ↩↩
-
WWDC 2025追加のオープンソースリポジトリの証拠:カスタムアタッチメントについては
Sources/Testing/Attachments/Attachment.swift、#expect(processExitsWith:)終了テストマクロについてはSources/Testing/ExitTests/。両者ともWWDC 2025で初めて登場し、Xcode 26+で提供されます。 ↩↩