Swift Digest
ST-0003 | Swift Evolution

.serializedトレイトをAPIとして公開する

Make .serialized trait API

Proposal
ST-0003
Authors
Dennis Weissmann
Status
Implemented (Swift 6.0)

このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら

01 何が問題だったのか

Swift Testing は、テストを並列に実行することを既定の挙動としています。各テストは独立しているのが望ましく、並列実行によって全体の実行時間が短くなり、暗黙の依存関係も発見しやすくなるためです。

しかし、現実のテストの中には、共有状態を扱うものや、複雑な依存関係を持つものなど、順序立てて実行されることを前提にしているものもあります。たとえば、共通のリソースに順番に書き込むパラメータ化テストや、スイート内で1つずつ走らせたい一連のテストです。このようなケースでは、複数のテストが同時に走ると結果が不安定になってしまいます。

一見すると、グローバルアクターを使えば順序を制御できそうに思えます。しかし、グローバルアクターが保証するのは「そのアクターに紐付くタスクが同時に複数走らない」ことだけです。タスクは await の地点で中断されることがあり、その間に同じアクター上の別のタスクが進む可能性があります。つまり、あるテストが完了するまで次のテストを開始させない、という保証はありません。

そのため Swift Testing には、内部的に .serialized というトレイトが用意されていて、指定したテストやスイートを直列に実行できるようになっていました。ただしこれは公開 API ではなく、利用者が直接使える状態ではありませんでした。

02 どのように解決されるのか

これまで内部的に存在していた .serialized トレイトを、公開 API として利用できるようにします。スイートやパラメータ化テストにこのトレイトを付けると、対象のテストが並列にではなく直列に実行されるようになります。

.serialized の効果

.serialized は、適用先によって次のように振る舞います。

  • スイート(@Suite)に付けた場合: そのスイートに含まれるテスト関数とサブスイートが、すべて直列に実行されます。さらにこのトレイトは再帰的に適用され、内側のスイートやそのテストにも自動的に伝播します。
  • パラメータ化テストに付けた場合: そのテストの各ケース(引数の組み合わせごと)が、並列ではなく1つずつ順番に実行されます。
  • パラメータ化されていない単一のテストに付けた場合: 効果はありません。

スイート全体ではなく単一のテストにだけ .serialized を付けても何も起きないのは、意図的な設計です。Swift Testing は並列化によるパフォーマンスを重視しており、それを下げる判断のハードルは高く設定されています。また、トレイトは「内側に向かって適用される」という原則があり、自分のトレイトが他のテストの実行条件にまで影響するのは予想外の挙動になってしまいます。スイート内の他のテストとの関係を制御したい場合は、スイート自身に .serialized を付ける必要があります。

なお、.serialized はあくまで対象のスイートやパラメータ化テストの内部の実行順を直列化するもので、それと無関係なテストの実行には影響しません。また、swift test--no-parallel を渡すなどしてテストの並列実行自体を無効にしている場合は、.serialized は何の効果も持ちません。

使い方

.serialized は、@Test@Suite の引数として渡します。

@Test(.serialized, arguments: Food.allCases) func prepare(food: Food) {
  // .serialized が付いているので、food ごとに1つずつ順番に実行される
}

@Suite(.serialized) struct FoodTruckTests {
  @Test(arguments: Condiment.allCases) func refill(condiment: Condiment) {
    // スイートが .serialized なので、condiment ごとに1つずつ順番に実行される
  }

  @Test func startEngine() async throws {
    // refill(condiment:) の実行中はこのテストは走らない。
    // 一方が終わってからもう一方が始まる。
  }
}

@Suite struct FoodTruckTests {
  @Test(.serialized) func startEngine() async throws {
    // パラメータ化テストでもスイートに .serialized も付いていないので、
    // .serialized は効果を持たない。
  }

  @Test func prepareFood() async throws {
    // 他のテストに付いた .serialized はこのテストに影響しない。
  }
}

API の形

.serializedParallelizationTrait という型のインスタンスとして公開されます。TestTraitSuiteTrait の両方に適合しているため、テスト関数とスイートのどちらにも適用できます。

public struct ParallelizationTrait: TestTrait, SuiteTrait {}

extension Trait where Self == ParallelizationTrait {
  public static var serialized: Self { get }
}

03 今後の見通し

並列実行の制御については、より高度で柔軟な仕組みを求める声があるとされています。たとえば「foo()bar() よりも先に実行する」といった、テスト間の依存関係を明示的に指定できるようにする方向が挙げられています。これは将来の構想であり、実現を約束するものではありません。