ハイライト
Xcode 27 はテストフレームワークの相互運用性をデフォルトで有効にし、同じテスト Target 内で XCTest と Swift Testing を混在させられるようにします。開発者はすべてのテストを一度に書き直すのではなく、段階的に移行できます。
主要内容
「全面書き直し」から「段階的移行」へ
昨年 Xcode 16 で Swift Testing が登場したとき、多くのチームは魅力を感じながらも手を出しにくい状況でした。成熟したプロジェクトには何千行もの XCTest コードがあることが多く、すべてを書き直すのはリスクが高く、ROI も高くありません。多くのチームは、新規プロジェクトには Swift Testing を使い、既存プロジェクトでは XCTest を保守し続ける、という選択をしました。
(03:16) この session の中心的な主張は、古いテストを一度に全部変えない、というものです。よく触るテストを毎回少しずつ移行し、残りの XCTest はそのまま動かします。同じテスト Target に二つのフレームワークのテストファイルを同時に含められ、Xcode はそれらをまとめて実行します。
相互運用性:二つのフレームワークの「翻訳層」
(05:48) 相互運用性 (interoperability) は Xcode 27 の新機能です。Swift Testing のテスト関数内で XCTFail のような XCTest API を呼べますし、XCTest の中で #expect のような Swift Testing API を呼ぶこともできます。フレームワークは内部で問題を適切なテストランナーへ自動的に報告します。
例として、何十もの XCTest から呼ばれている assertUnique という helper 関数があり、内部で XCTFail を使って失敗を報告しているとします。この helper を Swift Testing のテストからも再利用したい場合、相互運用性によりそのまま使えます。XCTFail の呼び出しは Swift Testing の issue 報告へ変換されます。
四つの相互運用モード
(07:43) Xcode 27 は、フレームワークをまたいだ問題の扱いを制御する四つのモードを提供します。
- Limited(Xcode 27 より前に作成されたプロジェクトのデフォルト):XCTest が発生させたフレームワーク横断の問題は warning として表示され、テストは通過します
- Complete(Xcode 27 の新規プロジェクトのデフォルト):フレームワーク横断の問題は error のままで、テストは失敗します
- Strict:XCTest が発生させたフレームワーク横断の問題は fatal error を直接起こし、Swift Testing API へ置き換えることを強制します
- None:相互運用性を完全に無効にし、二つのフレームワークは互いを認識しません
(09:05) おすすめの戦略は、まず Complete モードで問題を表面化させ、それから一つずつ置き換えることです。移行がかなり進んだら、CI で Strict モードへ切り替え、新しい XCTest API 呼び出しが Swift Testing のテストへ混ざるのを防ぎます。
パラメータ化テスト:ネストしたループを置き換える
(15:38) Swift Testing のパラメータ化テストは、XCTest 時代によくあった痛点を解決します。たとえば、すべての鳥について、40回から100回まで羽ばたかせたときの挙動をテストしたいとします。XCTest ではネストした for ループを書くことになります。
func testBirdsFlapWings() async throws {
for bird in Aviary.birds {
for count in (40...100) {
try await bird.flapWings(count: count)
}
}
}
問題は明らかです。ループ内のどこかが失敗したとき、どの for ループで落ちたかは分かっても、具体的にどの鳥のどの回数だったかは分かりません。デバッグにはログを追加するか、デバッガを使う必要があります。
(16:47) Swift Testing のパラメータ化テストは、ループを独立したテストケースへ展開します。
struct BirdTests {
@Test(arguments: Aviary.birds, 40...100)
func `鳥が正常に羽ばたく`(bird: Bird, count: Int) async throws {
try await bird.flapWings(count: count)
}
}
各パラメータの組み合わせは独立したテストケースになり、Test Navigator で展開して確認できます。さらに重要なのは、デフォルトで並列実行されるため、総実行時間が大幅に短くなることです。失敗時には、どの鳥がどの回数で失敗したかを Xcode が直接示してくれるため、推測する必要がありません。
Exit tests:クラッシュ経路をテストする
(18:08) Swift Testing の exit test により、preconditionFailure や fatalError を起こすコードパスをテストできます。フレームワークはテスト本体を子プロセスで実行するため、メインプロセスは影響を受けません。
extension BirdTests {
@Test func `名前が空の Bird はクラッシュする`() async throws {
await #expect(processExitsWith: .failure) {
_ = Bird(name: "")
}
}
}
(19:57) 実行後、Code Coverage で以前は赤く表示されていた preconditionFailure の行が緑になります。このクラッシュ経路はテストでカバーされたことになります。
詳細
既存の XCTest ファイルに Swift Testing テストを追加する
(04:18) 移行の第一歩では、新しいファイルを作る必要はありません。既存の .swift テストファイルの末尾で import Testing し、@Test 関数を書くだけです。
import Testing
@testable import DemoApp
@Test func `デフォルトの気候: 熱帯`() async throws {
let fruit = Fruit(name: "ココナッツ")
#expect(fruit.climate == .tropical)
}
重要な点:
@Testマクロは、これが Swift Testing のテスト関数であることを示します- バッククォート、つまり raw identifier により、関数名に空白や句読点を含められるため、テスト名を読みやすい自然言語で書けます
#expectはXCTAssertEqualを置き換え、失敗時には式を自動で展開して左右の値を表示しますXCTestCaseを継承する必要はなく、関数はファイルのトップレベルに置けます
helper 関数を XCTFail から Issue.record へ移行する
(05:03) XCTest 時代の helper 関数があるとします。
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("重複した名前: \(name)", file: file, line: line)
}
}
}
(10:12) これを Swift Testing にも対応する版へ書き換えます。
import Testing
func assertUnique(_ fruits: [Fruit], sourceLocation: SourceLocation = #_sourceLocation) {
var uniqueNames = Set<String>()
for name in fruits.map(\.name) {
if !uniqueNames.insert(name).inserted {
Issue.record("重複した名前: \(name)", sourceLocation: sourceLocation)
}
}
}
重要な点:
XCTFailをIssue.recordに置き換えますfile:line:引数をsourceLocation: SourceLocationに置き換えます- 更新後の helper が XCTest と Swift Testing の両方から呼ばれても、どちらでも正しい失敗位置が報告されます
テストをスキップする三つの書き方
(13:10) XCTest では XCTSkipIf でテストをスキップします。Swift Testing は同等の書き方を三つ提供します。
let isFall = false
// XCTest の書き方
func testSwallowFallMigration() async throws {
try XCTSkipIf(!isFall, "渡りの季節ではありません")
}
// Swift Testing: テスト本体の中で Test.cancel を呼ぶ
func testSwallowFallMigration() async throws {
if !isFall {
try Test.cancel("渡りの季節ではありません")
}
}
// Swift Testing の推奨: 条件を trait に移す
@Test(.enabled(if: isFall, "渡りの季節ではありません"))
func `ツバメの秋の渡り`() async throws {
// テストロジック
}
重要な点:
Test.cancelは、テスト本体の中で動的にスキップするかどうかを決められます.enabled(if:)trait はスキップ条件を関数シグネチャへ移し、テスト本体をきれいに保ちます- trait 方式では、関数本体を読まなくてもテストの「実行条件」が一目で分かります
continueAfterFailure の代わりに #require を使う
(13:41) XCTest では continueAfterFailure = false を設定し、最初の失敗した assertion でテストを止めます。Swift Testing では #require マクロで同じ効果を実現します。
func testExample() async throws {
#expect(Fruit.banana.climate == .temperate)
try #require(Fruit.banana == Fruit.plantain)
// #require が失敗すると error を throw し、後続のコードは実行されない
}
重要な点:
#expectは失敗しても後続の assertion を実行し続けます#requireは失敗時に error を throw し、テストをすぐ停止します- どの assertion を「強く停止」させ、どれを「柔らかく収集」するかを自由に選べます
Swift Package で相互運用モードを制御する
(12:15) Swift Package プロジェクトのデフォルトモードは tools version によって決まります。
swift-tools-version: 6.3以前:デフォルトは limited モードswift-tools-version: 6.4以降:デフォルトは complete モード
Terminal では環境変数で上書きできます。
SWIFT_TESTING_XCTEST_INTEROP_MODE=strict swift test
このコマンドは CI パイプラインで有用です。相互運用モードを strict に設定し、チームメンバーが Swift Testing のテスト内で XCTest API をこっそり呼ばないようにできます。
重要な示唆
既存プロジェクトのテストを「健康診断」する
- 何をするか: プロジェクト内の XCTest helper 関数をスキャンし、段階的に Swift Testing API へ移行します
- なぜ価値があるか: これらの helper は多くのテストから呼ばれていることが多く、移行するとすべての呼び出し側が自動的に恩恵を受けます。
Issue.recordはXCTFailより詳細な失敗情報を提供します - どう始めるか: カスタムの
assertSnapshotやassertUniqueなど、呼び出し回数が多い helper を一つ選び、XCTFailをIssue.recordに置き換え、テストを一度実行して挙動が一致することを確認します
パラメータ化テストでデータ駆動テストのループをなくす
- 何をするか: テストデータを
forループで回している XCTest を、@Test(arguments:)へ書き換えます - なぜ価値があるか: 並列実行で CI 時間が短くなります。失敗時も具体的なパラメータの組み合わせまで分かるため、デバッグ時の推測が不要になります
- どう始めるか: ネストしたループを持つテストを一つ見つけ、ループ変数を関数パラメータへ取り出し、ループ範囲を
arguments:に置きます
クラッシュ経路にテストを追加する
- 何をするか: exit test を使い、すべての
preconditionFailure、fatalError、assertionFailureの呼び出し箇所をカバーします - なぜ価値があるか: これらのコードパスは以前ほとんどテストできませんでした。今は
#expect(processExitsWith:)で安全に検証でき、Code Coverage も改善できます - どう始めるか: Xcode で Code Coverage を開き、赤く表示された
preconditionFailure行を見つけ、対応する exit test を書きます
CI でテストフレームワークの純度を強制する
- 何をするか: CI 設定に
SWIFT_TESTING_XCTEST_INTEROP_MODE=strictを追加します - なぜ価値があるか: 新しく書く Swift Testing のテストへ XCTest API が混ざるのを防ぎ、混成コードの蓄積を避けられます
- どう始めるか:
.github/workflows/または Jenkins pipeline のswift testコマンドの前に、この環境変数を追加します
raw identifier でテストの可読性を上げる
- 何をするか:
testDefaultClimateIsTropicalのような camelCase 名を、`デフォルトの気候: 熱帯`のような raw identifier に変えます - なぜ価値があるか: テスト名そのものがドキュメントになります。空白を含む文章は圧縮された camelCase より読みやすく、失敗ログに表示される名前も直感的です
- どう始めるか: 新しい Swift Testing テストを書くとき、略語にせず、自然言語の説明をバッククォートで囲みます
関連セッション
- Swift Testing をさらに活用する — カスタム traits、parallelization 制御、テスト整理戦略など、Swift Testing の高度な機能
- Xcode 27 — Xcode 27 のテスト関連 UI 改善と Coding Assistant による移行支援
- SwiftUI — SwiftUI プレビューと Swift Testing を組み合わせた統合テスト戦略
- SwiftData — データベース層のテストベストプラクティスと、Swift Testing による永続化検証
コメント
GitHub Issues · utterances