タグによるテスト実行のフィルタリング
Tag-based Test Execution Filtering
このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら↗。
01 何が問題だったのか
Swift Testing には、テストやスイートに任意の名前のタグを付けて分類する仕組みがあります。また、--filter / --skip コマンドラインオプションを使うと、テストのIDを正規表現でマッチさせて、実行するテストを絞り込んだりスキップしたりできます。しかし、この 2 つの機能は別々のものとして存在しており、タグを使ってテストをフィルタリング・スキップする ことはできませんでした。
テストには、狭い範囲を検証するユニットテストから、実際のデータベースを立ち上げる統合テスト、実行に長い時間がかかる UI テストまで、さまざまな種類があります。状況に応じて、毎回すべてのテストを swift test で走らせるのではなく、どのテストをいつ実行するかを制御したくなります。たとえば iOS プロジェクトで、機能ごとにターゲットを分け、それぞれにテストターゲットを用意しているような構成を考えます。
FoodTruck/
├── Package.swift
├── Sources/
│ ├── FoodDetailFeature/
│ ├── FoodListFeature/
│ ├── FoodTruck/
│ └── RootFeature/
└── Tests/
├── FoodDetailFeatureTests/
├── FoodListFeatureTests/
├── FoodTruckTests/
└── RootFeatureTests/
ローカルでの開発中に、すべてのテストパッケージにまたがる UI テストをまとめてスキップしたいとします。しかし、これまでのツールでは、すべてのパッケージで UI テストに一貫した命名規則を与え、なおかつ他のテストと名前が重ならないようにしておかない限り実現できませんでした。大規模なコードベースでは、これは現実的ではありません。テストグラフ(スイートの階層構造)とは別の軸で、ユーザーが定義した形でテストをグループ化する手段が求められていました。
タグはまさにテストグラフと直交してテストをまとめる仕組みですが、これまではフィルタリングの対象にできませんでした。そのため、タグによるフィルタリングをサポートする既存ツール(Visual Studio Code プラグインなど)は、SourceKit のインデックスでタグを検索し、マッチしたテストIDを集めて巨大な正規表現を組み立てる、という回りくどい実装に頼っていました。タグでフィルタリングする機能をネイティブに用意できれば、こうしたツールの実装も簡潔になります。
02 どのように解決されるのか
--filter / --skip オプションの引数に、特別な接頭辞 tag: を導入します。タグでフィルタリング・スキップしたいときは、引数を tag: で始めます。
swift test --skip tag:uiTest
tag: に続く部分(この例では uiTest)は、対象とするタグ名にマッチさせる正規表現です。通常の --filter / --skip と同じく、オプションを複数回指定すると、指定したタグの いずれか にマッチするテストをフィルタリング・スキップします。
swift test --skip tag:uiTest --skip tag:integrationTest
複数の --filter / --skip を指定した場合の挙動は or(いずれか)であり、and(すべて)ではない点に注意してください。指定したすべてのタグにマッチするテストだけを対象にする、という指定は現状ではできません。
id: 接頭辞による曖昧さの解消
SE-0451 で raw identifier が導入され、次のような関数名も書けるようになりました。
@Test func `tag:uiTest`() { /* ... */ }
このような名前のテストも引き続きマッチできるよう、id: というもう 1 つの接頭辞を導入します。id: は「テストのシンボル名に対してマッチする」ことを明示するための接頭辞で、続く部分は tag: と同様に正規表現として扱われます。
swift test --skip 'id:tag:uiTest'
id: は新しい挙動を加えるものではなく、Swift Testing がこれまでサポートしてきたフィルタリングそのものです。接頭辞を省略することもでき、既知の接頭辞(tag: / id:)のいずれも付いていない場合は id: が指定されたものとみなされます。これにより、既存の使い方はそのまま動作します。
raw identifier をタグ名に使う場合
逆に、raw identifier をタグ名として使うこともできます。
extension Tag {
@Tag static var `some tag with spaces`: Self
}
@Test(.tags(.`some tag with spaces`))
func myTest() { /* ... */ }
このようにスペースを含むタグ名でフィルタリングするときは、引数全体をシングルクォートで囲み、tag: の後ろをそのまま 1 つのタグ名として扱わせます。
swift test --filter 'tag:some tag with spaces'
raw identifier を区切るためのバッククォートはシンボル名の一部ではないため、次のようにバッククォートごと指定してもマッチしません。
swift test --filter 'tag:`some tag with spaces`' # INVALID: シンボルにマッチしない
引数全体がバッククォートで囲まれている場合(より正確には正規表現 /^`[^`]*`$/ にマッチする場合)は、ユーザーが raw identifier を意図したものと判断し、エラーメッセージを表示したうえでバッククォートを取り除きます。
Backticks aren't a valid part of a Swift symbol. Replacing '`some tag with spaces`' with 'some tag with spaces'.
互換性についての注意
テスト関数やスイートの名前に文字列 tag: が含まれている場合、tag: で始まるフィルタリング・スキップ引数の挙動はこの提案によって変わり、正規表現ではなくタグとして解釈されます。ただし、シンボル名に tag: が現れるのは前述の raw identifier を使った場合に限られるため、影響を受けるのは比較的まれなケースです。この場合は id: 接頭辞を使えば、これまで通りシンボル名に対してマッチできます。
03 今後の見通し
提案では、タグによるフィルタリングをきっかけとして、さらに表現力を高める方向がいくつか挙げられています。いずれも将来の構想であり、実現を約束するものではありません。
tag: 以外のフィルタリング軸
タグによるフィルタリングは汎用的ですが、これを足がかりに「他に何でフィルタリング・スキップできるとよいか」という議論が生まれます。たとえば、プロトコルへの適合や継承関係でテストを絞り込みたいケースが考えられます。スイートの祖先の型は、その型が持つ振る舞いや契約を通じて「このテストを今の文脈で実行すべきか」を示す、より自然なシグナルになりえます。将来的には tag: 以外の接頭辞演算子を追加していくことが構想されています。
真偽値の組み合わせによるフィルタ
Swift Testing の内部では、すでにテストフィルタの任意の真偽値による組み合わせ(and / or などの論理結合)がサポートされています。これをコマンドラインから扱えるように公開することも、将来の検討対象として挙げられています。