01 何が問題だったのか
Swift Testing には、当初から「テストを一定回数繰り返す」あるいは「特定の失敗・成功条件が満たされるまで繰り返す」ためのテスト反復機能が用意されています。Swift Testing のエントリポイントに --repetitions や --repeat-until を渡すツールではこの機能を活用でき、フレーキーなテストの再現や、外部依存の不安定さに起因する失敗のデバッグに役立ちます。一方で、swift test コマンドからはこの機能を直接利用する手段が用意されておらず、コマンドラインから手軽にテストを反復させたいケースに応えられていませんでした。
しかし、既存のテスト反復機能には粒度の問題もありました。反復条件はテストターゲット全体に対して評価され、ターゲット内のいずれかのテストケースが条件を満たすと、そのターゲットに含まれるすべてのテストが再実行されてしまっていたのです。たとえば、大きなパラメータ化テストスイートのうち 1 つのテストケースだけが失敗した場合でも、ターゲット全体が丸ごと再実行されることになります。
この挙動には、次のような問題があります。
- 関係のないテストまで再実行されるため、テスト時間が不必要に長くなります。
- XCTest では失敗したテストメソッドだけが再実行されるため、挙動が一致しません。
- 「issue が記録されている間はテストを繰り返す(repeat tests while issue recorded)」という説明から開発者が想像する挙動、すなわち「issue が記録されたテストだけが繰り返される」という直感とも食い違っています。
加えて、反復に関する情報は testStarted / testEnded などのテストイベントとは独立した「グローバルな iteration イベント」として通知されていたため、各テストの実行結果が「何回目の反復なのか」を関連付けて扱うのも難しい状態でした。
02 どのように解決されるのか
Swift Testing の反復挙動を、テストターゲット全体ではなく 個々のテストケース単位 で評価するように変更します。あわせて、反復に関する情報をテストイベント自体に埋め込み、グローバルな iteration イベントは廃止します。
挙動の変更
各テストケースの実行が終わるたびに反復条件が評価され、条件を満たしたテストケースのみが再実行されます。これにより、
- フレーキーなテストケースだけを繰り返すことができ、関係のないテストの再実行が起きません。
- XCTest が失敗したテストメソッドのみを再実行する挙動と整合します。
- 「issue が記録されたテストだけを繰り返す」という直感どおりの動作になります。
従来の「ターゲット全体を繰り返す」挙動は完全に削除されます。コンソール出力もこの新しい単位に合わせて調整されます。
なお、これは観測可能な挙動の変化を伴います。たとえばシリアライズされたテストスイートが「特定の順序ですべてのテストが 1 回ずつ実行される」ことに依存していた場合、一部のテストだけが複数回実行されることで、その依存が壊れる可能性があります。ただし、テスト関数間にこのような暗黙の依存を持たせることはアンチパターンであり、避けるべきものとされています。
イベントへの iteration の追加
JSON イベントスキーマ(バージョン "6.4" を予定)では、各イベントレコードに 1 始まりの iteration フィールドが追加されます。
{
["attachment": <attachment>,] ; the attachment (if kind is "valueAttached")
"messages": <array:message>,
["testID": <test-id>,]
+ ["iteration": <number>] ; the one-indexed test iteration (if event is posted during test execution).
}
iteration は、テスト実行中に発行されるイベントであれば、反復ポリシーが .none に設定されている場合でも提供されます(その場合は常に 1 になります)。Tools SPI 側の型にも同等の情報が追加されます。
ツール統合
テスト反復をサポートしたいツールは、これまでどおり Swift Testing のエントリポイントに --repetitions や --repeat-until といったコマンドライン引数を渡すことで反復挙動を提供できます。これらが指定されていない場合、JSON イベント中の iteration の値はすべて 1 になります。
swift test への反復オプションの追加
これまで swift test コマンドからは反復機能を直接利用できませんでしたが、本Proposalで次の 2 つのフラグが追加されます。
| フラグ | 内容 |
|---|---|
--maximum-repetitions |
テストを反復させる最大回数 |
--repeat-until |
反復条件。pass または fail を指定する。省略した場合は無条件で --maximum-repetitions 回まで反復する |
たとえば、フレーキーなテストを最大 5 回まで「失敗するまで」繰り返したい場合は、次のように実行します。
swift test --maximum-repetitions 5 --repeat-until fail
なお、Swift Testing のエントリポイント側のフラグは引き続き --repetitions という名前ですが、エンドユーザー向けの swift test ではより意図が明確な --maximum-repetitions という名前を採用します。--repeat-until の名前は両者で共通です。
03 今後の見通し
将来の発展として、以下のようなアイデアが挙げられています。
反復挙動を指定するトレイト
特定のテストにだけ反復挙動を割り当てたいというニーズがあります。たとえば CI 上で、ネットワーク起因などで一時的に失敗しやすいテストだけを限定的にリトライしたい場合です。これを実現するため、テストごとに反復挙動を指定できるトレイトを追加することが検討されています。
@Test(.repeating(.whileIssueRecorded, maximumIterations: 5, comment: "This might get transiently disconnected")))
func somethingNetworkBound() {
let value = await downloadSomethingFromTheInternet()
}
ただし、グローバルな設定パラメータとの相互作用や、パラメータ化テストの一部のテストケースだけを繰り返すための構文をどうするかなど、まだ詰めるべき点が残っています。
実行時に現在の iteration を取得する API
クライアントが現在の反復回数を実行時に読み取れるようにする API も挙がっています。これがあれば、たとえばリトライ時にだけ詳細なログを有効化するような、デバッグ向けの工夫が可能になります。
@Test
func ableToConnectToSocket() {
let value = await socket.connect(enableVerboseLogging: Test.currentIteration > 1)
#expect(...)
}
これらはいずれも将来の構想であり、実現を約束するものではありません。