テストのタイムアウト時間の粒度を制限する
Constrain the granularity of test time limit durations
このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら↗。
01 何が問題だったのか
テストの中には、テスト対象のコードや、テスト自体の問題によって処理が前進しなくなり、ハングしてしまうものがあります。Swift Testing は、こうしたハングへの対策として .timeLimit トレイトを提供しており、指定した時間内にテストが終わらない場合は失敗として扱う仕組みを備えています。
@Test(.timeLimit(.minutes(60)))
func testFunction() { ... }
問題は、これまでの .timeLimit トレイトには2種類のオーバーロードが存在していたことです。1つは Swift 標準の Duration 型を受け取るもので、ミリ秒や秒など任意の粒度で値を指定できました。もう1つは TimeLimitTrait.Duration を受け取るもので、最短1分・1分刻みに制約された粒度になっていました。
Swift.Duration を受け取るオーバーロードがあると、ごく短いタイムアウトを指定できてしまいます。しかし、テストが走る環境はデスクトップ・CI・仮想化環境などで性能が大きく異なり、特に CI や仮想ハードウェア上ではテストの実行が著しく遅くなることがあります。短いタイムアウトを設定すると、テスト対象に問題がなくても時間切れで失敗してしまい、CI パイプライン全体の再実行を招くなど、コストの高い不安定さの原因になります。
そもそも .timeLimit は、ハングや病的に長く走るテストを検知するための仕組みであり、「実行時間がわずかに伸びていないか」を監視するためのものではありません。タイムアウトによってテストが落ちることはきわめて稀であるべきで、API 自体がドキュメントを読まなくても妥当な値を選びやすい形になっているのが望ましい状態です。
02 どのように解決されるのか
.timeLimit トレイトが受け取る型を、TimeLimitTrait.Duration に一本化します。これまで SPI として存在していたこの型を正式な API として公開し、Swift.Duration を受け取るオーバーロードは使えないようにします。これにより、タイムアウトとして指定できるのは最短1分・1分刻みの値だけに制約されます。
TimeLimitTrait.Duration の形
TimeLimitTrait.Duration は、minutes(_:) というファクトリメソッドだけを持つ Sendable な構造体です。
@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
public struct TimeLimitTrait: TestTrait, SuiteTrait {
public struct Duration: Sendable {
public static func minutes(_ minutes: some BinaryInteger) -> Self
}
public var timeLimit: Swift.Duration { get set }
}
ファクトリメソッドが minutes(_:) のみであることによって、次の2つが構造的に保証されます。
- 1分未満の短いタイムアウトを作れない
- 1分より細かい粒度でタイムアウトを作れない
これらは API 自体がその意図(ハング検知のための粗い粒度のタイムアウトである)を表現するための重要な設計です。
.timeLimit(_:) 自体は次のように Trait のエクステンションで定義されます。
public static func timeLimit(_ timeLimit: Self.Duration) -> Self
使い方
呼び出し側のコードは従来とほぼ同じで、.minutes(_:) を使ってタイムアウトを指定します。
@Test(.timeLimit(.minutes(60)))
func serve100CustomersInOneHour() async {
for _ in 0 ..< 100 {
let customer = await Customer.next()
await customer.order()
// ...
}
}
パラメータ化テストに .timeLimit を付けた場合、タイムアウトは各ケースごとに個別に適用されます。1つのテストに複数の .timeLimit が付いている場合は、その中で最も短いものが採用されます。また、テスト実行ツール側で「1テストあたりの最大タイムアウト」が設定されている場合、実際のタイムアウトはトレイトで指定した値とその上限のうち短い方になります。
誤用を防ぐための診断
秒・ミリ秒など分以外の単位を使おうとした場合に分かりやすい診断を出すため、TimeLimitTrait.Duration には unavailable としてマークされたオーバーロードが用意されています。これらは実行されることはなく、コンパイル時のメッセージ表示のためだけに存在します。
@available(*, unavailable, message: "Time limit must be specified in minutes")
これにより、たとえば .seconds(30) のような書き方をすると、「タイムアウトは分単位で指定してください」という旨の診断が表示されます。
03 今後の見通し
テストホスト機の性能に応じてタイムアウトをスケールさせる仕組みや、CI かローカルかといった実行環境を検出して環境ごとに異なるタイムアウトを使い分けられる仕組みを導入することで、より細かい粒度のタイムアウトを安全に扱えるようにする方向が示されています。これらは将来の構想であり、実現を約束するものではありません。