modernize-tests について
Swift テストをモダナイズするスキルです。移行可能な XCTest コードを Swift Testing に移し、既存テストも推奨パターンに更新します。Swift テストスイートの保守担当者向けに、移行手順と XCTest に残すべきケースを説明します。
主な機能
- 01XCTest テストを Swift Testing に移行
- 02アサーションを期待値に置換
- 03非同期の期待値とスキップを更新
- 04並行処理と共有状態に対応
- 05移行できないテストを特定
活用シーン
- 01テストクラスを段階的に移行
- 02従来のアサーションを更新
- 03非同期テストの期待値を変換
- 04UI・性能テストを XCTest に維持
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ tartinerlabs/ skills --skill modernize-testsインストール後、Claude Code で/modernize-testsを入力して読み込まれたか確認します。
詳細
テストのモダナイズ
テストのモダナイズとは、XCTest から Swift Testing への移行と、既存の Swift Testing テストを推奨されるパターンに更新することの、2 つの対応を指します。
可能な場合は、XCTest を Swift Testing に移行してください。ただし、すべての XCTest を Swift Testing に移行できるわけではありません。
- UI テスト(XCUIAutomation を使用するテスト)は Swift Testing では記述できないため、XCTest のままにする必要があります。
- パフォーマンス測定に
measure { ... }系の API を使用する XCTest は移行できません。ただし、XCTest のパフォーマンス API を使用しない XCTestCase 内のその他のテストメソッドは移行できます。
移行リファレンス
インポート
import XCTest を import Testing に置き換えます。段階的な移行中にテストが混在しているファイルでは、両方をインポートできます。
import XCTest を削除する際は、そのファイルで Foundation の型(URL、CharacterSet、ProcessInfo、Data など)を使用しているか確認してください。XCTest は Foundation を再エクスポートするため、必要に応じて import Foundation を追加します。
テストクラスからスイートへ
XCTestCase の継承を削除します。クラスよりも構造体を優先してください。
final class FoodTruckTests: XCTestCase { ... }->struct FoodTruckTests { ... }
setUp/tearDown コードを init/deinit に移動する
override func setUp() を init()(async throws も可)に置き換えます。override func tearDown() は deinit に置き換えます。deinit が必要な場合は、構造体には deinit がないため、struct の代わりに actor または final class を使用します。保存プロパティを暗黙的にアンラップされるオプショナル型にしないよう変更し、初期値の代入を setUp から移して、インラインで初期化するか、初期化が複雑な場合はイニシャライザー内で行います。
struct MyTests {
var fixture = Fixture()
mutating func `Fixture behaves as expected`() {
#expect(fixture.doSomething())
}
}
インスタンス変数を関数本体に移すのは避けてください。ノイズの原因になることがあります。Swift Testing は各テストの実行前にイニシャライザーを再度呼び出します。
値セマンティクスを持つインスタンス変数をテスト内で変更する場合、テスト関数に mutating を付ける必要があることがあります。
テストメソッド
test という名前の接頭辞を @Test 属性に置き換えます。置き換え後のテスト名に複数のキャメルケースの単語が含まれる場合は、文としての大文字・小文字表記のテスト名を使った生識別子を使用します。
func testEngineDoesNotStall() { ... }->@Test funcエンジンは停止しない() { ... }func testIgnition() { ... }->@Test func ignition() { ... }
テスト関数には async、throws、async throws を指定できます。また、@MainActor を使ってグローバルアクターに隔離できます。
アサーションから期待値へ
XCTest から Swift Testing にテストを移行する際は、次の対応関係を適用します。
XCTAssert(x), XCTAssertTrue(x) -> #expect(x)
XCTAssertFalse(x) -> #expect(!x)
XCTAssertNil(x) -> #expect(x == nil)
XCTAssertNotNil(x) -> #expect(x != nil)
XCTAssertEqual(x, y) -> #expect(x == y)
XCTAssertNotEqual(x, y) -> #expect(x != y)
XCTAssertIdentical(x, y) -> #expect(x === y)
XCTAssertNotIdentical(x, y) -> #expect(x !== y)
XCTAssertGreaterThan(x, y) -> #expect(x > y)
XCTAssertGreaterThanOrEqual(x, y) -> #expect(x >= y)
XCTAssertLessThanOrEqual(x, y) -> #expect(x <= y)
XCTAssertLessThan(x, y) -> #expect(x < y)
try XCTUnwrap(x) -> try #require(x)
XCTAssertEqual(_:_:accuracy:) に直接相当するものはありません。浮動小数点演算を直接使用してください。
エラー
エラー型が Equatable で、正確な値がわかっている場合は、特定のエラー値を確認することを優先してください。
XCTAssertThrowsError(try f())
->
#expect(throws: (any Error).self) {
try f()
}
XCTAssertThrowsError(try f()) { error in
XCTAssertEqual(error, specificError)
}
->
#expect(throws: specificError) {
try f()
}
XCTAssertThrowsError(try f()) { error in
// Check error
}
->
let error = #expect(throws: (any Error).self) {
try f()
}
// Check error
XCTAssertNoThrow(try f())
->
#expect(throws: Never.self) {
try f()
}
continueAfterFailure
デフォルトでは continueAfterFailure は true です。これは、期待値に反してもテストの実行が停止しないことを意味します。
一部の XCTestCases では continueAfterFailure = false が設定されています。この場合、XCTAssert ファミリーの関数は
テストの実行を停止する Objective-C 例外をスローします。
テストメソッドで continueAfterFailure = false を設定した場合、この動作を維持するため、以降のすべてのアサーションで #expect(x) ではなく try #require(x)
を使用する必要があります。try #require(x) を追加する場合は、該当するメソッドに throws を追加してください。
setUp で continueAfterFailure = false を設定した場合、try #require(x) への変換は、そのクラスのすべてのテストメソッド内のすべてのアサーションに適用する必要があります。
Issue.record/XCTFail を期待値に置き換える
支障のない箇所では、Issue.record または XCTFail の使用を、テスト終了後の動作(continueAfterFailure を考慮)に応じて #expect または #require に置き換えてください。
場合によっては、期待値自体の内容で失敗の原因を十分に説明できるため、コメントは冗長になります。
たとえば、次の構造は以下のように置き換えてください。
guard let object = somethingOptional() else {
Issue.record("Could not get object")
return
}
guard object.isAvailable() else {
Issue.record("Object not available")
return
}
if !object.performOperation() {
Issue.record("Failed to perform operation")
}
->
let object = try #require(somethingOptional(), "Could not get object")
try #require(object.isAvailable())
#expect(object.performOperation())
非同期の期待値を確認に置き換える
XCTestExpectation + fulfill() + await fulfillment(of:) を confirmation() に置き換えてください。
// Before
let exp = expectation(description: "...")
handler = { exp.fulfill() }
doWork()
await fulfillment(of: [exp])
// After
await confirmation("...") { confirm in
handler = { confirm() }
doWork()
}
assertForOverFulfill = false と expectedFulfillmentCount を使用する場合は、範囲を指定してください。
await confirmation("...", expectedCount: 10...) { confirm in ... }
テストのスキップ
XCTSkipIf/XCTSkipUnless を、テストまたはスイートのトレイトに置き換えてください。
try XCTSkipIf(condition)->@Test(.disabled(if: condition))try XCTSkipUnless(condition)->@Test(.enabled(if: condition))
テストの途中で throw XCTSkip("reason") を使う場合は、try Test.cancel("reason") に置き換えてください。
OS バージョンまたはプラットフォームの利用可否を確認してスキップする場合は、.enabled(if:) ではなく、テスト関数に @available 属性を追加してください。
既知の問題
XCTExpectFailure("...", ...) { ... } を withKnownIssue("...") { ... } に置き換えてください。
断続的に発生する失敗については、.nonStrict() オプション(または省略形の strict: false パラメーター)を isIntermittent: true に置き換えてください。
条件指定/照合には、when: と matching: のパラメーターを使用してください。
withKnownIssue("...") {
try riskyOperation()
} when: {
shouldExpectFailure
} matching: { issue in
issue.error != nil
}
並行処理と直列実行
XCTest はデフォルトで、同期テストをメインアクター上で実行し、スイート内では順番に実行します。Swift Testing はすべてのテスト関数を任意のタスク上で並列に実行します。XCTest でのテストがメインアクターの分離に明示的に依存していた場合にのみ @MainActor を追加し、テストが共有状態に依存する場合は @Suite(.serialized) を追加してください。
添付ファイル
XCTAttachment + self.add(attachment) を Attachment.record(value) に置き換えてください。添付する型は Attachable に準拠している必要があります(Foundation をインポートしている場合、Codable および NSSecureCoding 型は自動的に準拠します)。
モダナイズのガイドライン
- XCTest から移行する場合は、テストクラスを一度に 1 つずつ移行してください。移行中は、1 つのファイルに XCTest のテストと Swift Testing のテストが共存してもかまいません。
deinit(tearDown)が必要な場合を除き、テストスイートにはstructを優先してください。その場合はactorまたはfinal classを使用してください。@Testを追加する際は、メソッド名からtestプレフィックスを削除してください。文のように読める長めのテスト名には、読みやすさを高めるため、生の識別子構文を使用してください。例:@Test funcAuthenticate, fetch summary, then check count() { ... }。- 生の識別子構文は、文のように読める複数語の名前にのみ使用してください。
setUpを移行する際は、暗黙的にアンラップされるオプショナルプロパティを、インプレースで初期化する非オプショナルプロパティに変換してください。初期化が複雑な場合、エラーをスローする可能性がある場合、または非同期の場合は、initで初期化してください。#expectまたは#requireに変換できる、明示的なXCTFail/Issue.recordの呼び出しがないか確認してください。try #requireの呼び出しを#expectに変更しないでください。テストの動作が変わります。@MainActorは、XCTest の暗黙的なメインアクター分離に明示的に依存していたテストにのみ追加してください。不必要に追加しないでください。- 入力をループするテストや、同じロジックで繰り返し実行されるテストが多数ないか確認し、
@Test(arguments:)を使ったパラメーター化テストに変換してください。 - テスト間で可変の共有状態があるテストスイートには、
@Suite(.serialized)を追加し、structの代わりにactorまたはclassを使うことを検討してください。 #_sourceLocationのような、アンダースコアで始まるシンボルの使用を新たに導入しないでください。公開 API のみを使用してください。ソース位置には、常に完全なSourceLocation(fileID:filePath:line:column:)イニシャライザーを使用してください。- テストスイートですでに
#_sourceLocationが使われている場合は、モダナイズの一環として既存の使用箇所を置き換えないでください。 - モダナイズ作業が複雑な場合は、必要に応じて複数のエージェントに分担してください。