はじめに
こんにちは!システム開発部のYです。
Xcode 27で相互運用モードが追加され、XCTestからSwift Testingへの移行がしやすくなりました。
本記事では、以下のセッション内容をまとめています。詳細については、WWDCセッションをご参照ください。
Migrate to Swift Testing(WWDC2026 Session 267)
Swift Testingの振り返り
Swift Testingのテストは @Test マクロで宣言し、アサーションには #expect マクロを使います。
import Testing
@testable import DemoApp
@Test func `Default climate: tropical`() async throws {
let fruit = Fruit(name: "Coconut")
#expect(fruit.climate == .tropical)
}XCTestと比較した際に以下の点が挙げられます。
testプレフィックスの命名規則は不要です。@Testマクロを関数に付与するだけでテストとして認識されます。XCTAssertEqual/XCTAssertTrueなど多数のアサーション関数を、単一の#expectマクロに置き換えられます。- バッククォートで囲むことで、
`Default climate: tropical`のように、スペースや記号を含む自然な文章をテスト名にできます。 - 無条件で失敗させたい場合は、
XCTFail()の代わりにIssue.record()を使います。
Swift TestingはSwift Concurrencyにネイティブ対応しており、テストがデフォルトで並列実行されるため、テストスイート全体の実行時間の短縮が期待できます。
移行戦略
セッションで推奨されているのは、リスクを最小化する段階的なアプローチです。
- 既存のXCTestはそのまま残す – 動いているテストを無理に置き換えない
- 新規テストはSwift Testingで書く
- 頻繁に更新するテストから移行する – 触る機会が多いコードから順に
- 一度に移行するのは数個ずつ – 変更を小さく保ち、問題の切り分けを容易にする
この「共存期間が長くなる」戦略を現実的にするのが、次に紹介する相互運用性です。
テストフレームワーク相互運用性
移行中に起きること
移行を進める間、テストターゲットの中ではXCTestとSwift Testingのテストが同居します。
この時扱いに困るのが共有ヘルパー関数です。中身は XCTFail などXCTestのAPIで書かれているのに、呼び出し元にはSwift Testingへ移行済みのテストが増えていくからです。
func testUniqueFruitNames() async throws {
assertUnique(Market.fruits + [Fruit.lychee])
}
// TestHelpers.swift
func assertUnique(_ fruits: [Fruit], file: StaticString = #filePath, line: UInt = #line) {
var uniqueNames = Set<String>()
for name in fruits.map(\.name) {
if !uniqueNames.insert(name).inserted {
XCTFail("Duplicate name: \(name)", file: file, line: line)
}
}
}このヘルパーを移行済みのテストから呼ぶと、Swift Testingのテストの中でXCTestのAPI(XCTFail)が発火することになります。
@Test func `Unique fruit names`() {
assertUnique(Market.fruits + [Fruit.lychee]) // 中で XCTFail が発火したら?
}このように、「issueを報告するAPI」と「そのAPIが呼ばれたテスト」が異なるフレームワークに属している場合に生じるissueを、セッションではクロスフレームワークissueと呼んでいます。
Xcode 27のテストフレームワーク相互運用性は、このクロスフレームワークissueを正しく検出・報告する仕組みです。
これがあるおかげで、ヘルパー関数の書き換えを待たずにテスト本体の移行を進められます。
4つの相互運用モード
クロスフレームワークissueをどれだけ厳しく扱うかは、4つの相互運用モードで決まります。
「Swift Testingのテストの中で、XCTestのAPIが失敗を報告した時、どうなるか」を軸に、緩い順で並べるとこうなります。
| モード | issueの扱い | テストの結果 | どんな時に使う |
| None | 報告されない (両方向とも) | ✅ 成功してしまう (失敗が消える) | 非推奨 一時的な回避のみ |
| Limited | ⚠️ 警告として記録 | ✅ 成功のまま | Xcode 27より前に作成したテストプランのデフォルト |
| Complete | ✖️ エラーとして記録 | ✖️ 失敗する | 新規プロジェクトのデフォルト。移行中の推奨 |
| Strict | 💥 fatal error | 💥 実行が即停止 | 移行完了の最終チェック用 |
ざっくり言うと、None=「見ない」、Limited=「気づける」、Complete=「失敗にする」、Strict=「許さない」の4段階です。
※この表は「Swift Testingのテスト内でXCTestのAPIが失敗を報告した場合」の挙動です。逆方向(XCTestのテスト内でSwift Testing APIが失敗)は、Noneを除くどのモードでも失敗として扱われます。
Xcode 27より前に作成されたテストプランはLimitedモードを引き継ぐため、移行を始めたらまずCompleteへ引き上げるのが実質的な前提です。そうしないと、ヘルパー経由の失敗が警告どまりになり、テストは緑のまま通ってしまいます。
モードの設定方法
相互運用モードは、次の3つの方法で設定できます。
Xcode
テストプラン設定の Test Execution セクションで、いつでも変更できます。

Swift Package
Swift 6.4ツールチェーンでは、swift-tools-version が6.3以前のパッケージにはLimitedモードが適用されます。
Completeモードをデフォルトで使用するには、swift-tools-version を6.4以降に更新します。
CLI
環境変数 SWIFT_TESTING_XCTEST_INTEROP_MODE を使うと、設定にかかわらずその場で上書きできます。
値にはモード名を小文字で指定します。
SWIFT_TESTING_XCTEST_INTEROP_MODE=strict swift test相互運用でサポートされるAPI
どんなAPIでも相互運用できるわけではありません。対象は次のAPIに限られます。
| フレームワーク | サポートされるAPI |
| XCTest | XCTFail、すべてのテストアサーション(XCTAssert...)、XCTExpectFailure |
| Swift Testing | Issue.record、#expect、#require、known issue API(withKnownIssue)、Test.cancel() |
相互運用は双方向なので、Swift Testing側のAPIはXCTestのテストの中で呼んでも機能します。
たとえば withKnownIssue を使えばXCTestのアサーション失敗を「既知の問題」としてマークでき、Test.cancel() を使えばXCTestのテストケースをスキップできます。
最終形:ヘルパーをSwift Testing APIに置き換える
相互運用性はあくまで移行期間中の一時的な手段であり、準備ができたら、ヘルパー本体もSwift Testing APIに置き換えます。
ポイントは XCTFail を Issue.record に置き換えることです。
import Testing
// Before: func assertUnique(_ fruits: [Fruit], file: StaticString = #filePath, line: UInt = #line)
func assertUnique(_ fruits: [Fruit], sourceLocation: SourceLocation = #_sourceLocation) {
var uniqueNames = Set<String>()
for name in fruits.map(\.name) {
if !uniqueNames.insert(name).inserted {
Issue.record("Duplicate name: \(name)", sourceLocation: sourceLocation)
}
}
}書き換えたヘルパーは、まだ移行していないXCTestのテストから呼ばれても問題ありません。
相互運用が有効な場合、XCTestのテスト内でSwift Testing APIが報告した失敗も、テストの失敗として扱われます。
よくある移行パターン
テストのスキップ: XCTSkip → Test.cancel() / .enabled(if:)
// XCTest
func testSwallowFallMigration() async throws {
try XCTSkipIf(!isFall, "Wrong season for migration")
// ...
}Swift Testingでは2通りの書き方があります。まず、実行時に条件分岐する場合は Test.cancel() を使います。
@Test func `Swallow fall migration`() async throws {
if !isFall {
try Test.cancel("Wrong season for migration")
}
// ...
}もう1つは .enabled(if:) トレイトを使う書き方です。スキップ条件を @Test の宣言側に書くので、見ただけで「いつ動くテストか」が分かります。
@Test(.enabled(if: isFall, "Wrong season for migration"))
func `Swallow fall migration`() async throws {
// ...
}失敗時に停止: continueAfterFailure = false → #require
XCTestでは continueAfterFailure = false でアサーション失敗時にテストを中断していました。
Swift Testingでは、中断させたいアサーションにだけ #require を使います。
@Test func example() async throws {
#expect(Fruit.banana.climate == .temperate) // 失敗しても続行
try #require(Fruit.banana == Fruit.plantain) // 失敗したらここで中断
// これ以降は #require が通った場合のみ実行される
}テスト全体のフラグではなく、アサーション単位で継続/中断を制御できるのがSwift Testing流です。
移行ついでに強化: パラメータ化テスト
移行はテストを見直す絶好の機会でもあります。XCTestにはパラメータ化の仕組みがなかったため、複数の入力を検証するテストはループで書くしかありませんでした。その書き方をSwift Testingにそのまま持ち込むと、こうなります。
// Before: ネストしたループ — どの入力で失敗したか分かりにくい
@Test func `Birds flap wings successfully`() async throws {
for bird in Aviary.birds {
for count in (40...100) {
try await bird.flapWings(count: count)
}
}
}動きはしますが、これでは移行した意味が半分しかありません。@Test(arguments:) に書き換えると、ループが消えて入力の組み合わせが宣言になります。
// パラメータ化テスト
@Test(arguments: Aviary.birds, 40...100)
func `Birds flap wings successfully`(bird: Bird, count: Int) async throws {
try await bird.flapWings(count: count)
}パラメータ化することで
- 各組み合わせがデフォルトで並列実行されるため、実行時間の短縮が期待できる
- Test Navigatorに組み合わせごとの成否が個別に表示される
- 失敗した特定の入力値が一目で分かる
クラッシュもテストできる: Exit tests
preconditionFailureなど、プロセスが終了することを期待するコードは、XCTestでは標準APIを使って直接テストすることが困難でした。Swift TestingのExit testsなら検証できます。
// テスト対象: Bird.init(...) 内
if name.isEmpty {
preconditionFailure("Bird name cannot be empty")
}extension BirdTests {
@Test func `Bird with empty name crashes`() async throws {
await #expect(processExitsWith: .failure) {
_ = Bird(name: "")
}
}
}- テスト本体は子プロセスで実行されるため、テストランナー自体はクラッシュしない
- プロセスの終了ステータスで成否を判定する
- コードカバレッジにも対応
- 対応プラットフォームは macOS / Linux / FreeBSD / OpenBSD / Windows。iOSなどのApple製プラットフォーム(シミュレータ含む)では利用不可
XCTestを使い続けるべきケース
以下のケースでは引き続きXCTestを使用します。
- UIオートメーションテスト: 画面操作を自動化する
XCUIApplicationなどのAPIは、XCTestにしかありません - パフォーマンステスト: 実行時間などを計測する
measureAPIも、XCTestにしかありません - Objective-C例外をスローするコードのテスト: Objective-Cで書いたXCTestを使う必要があります。Swiftコードは、Swiftで書いたXCTestも含めて、Objective-C例外を安全に処理できないためです
まとめ
WWDC2026のセッション「Migrate to Swift Testing」から、Xcode 27の相互運用モードを中心にSwift Testingへの移行の進め方をまとめました。
Xcode 27より前に作成されたテストプランはLimitedモードを引き継ぎ、失敗が警告どまりでテストが通ってしまうため、まずCompleteモードへ引き上げることが移行のスタートラインになると感じました。
この記事が少しでも参考になれば幸いです。詳細はWWDCのセッションをご参照ください。
参考リンク
- Migrate to Swift Testing — WWDC2026 Session 267
- Swift Testing 公式ドキュメント
- swiftlang/swift-testing (GitHub)
