SwiftTestingのparameterizedテストとTraitsの書き方

# はじめに
SwiftTestingの特徴的な要素である、パラメタライズドテストと各種Traitを一覧にしてそれぞれ使い所を考えます。
swift6.0.3時点の内容です。

# テストの基本的な構造
```swift
import Testing

@Test func videoMetadata() {
    // ...
}
```
[Swift Testingについて - WWDC24 - ビデオ - Apple Developer](https://developer.apple.com/jp/videos/play/wwdc2024/10179/)

# パラメタライズドテスト
Testマクロの引数に配列などのコレクションを渡すことで入力の異なる複数のテストを簡潔に書くことができます。
同じ引数の組み合わせが生じるとエラーになります。

## 1つの配列のループ
最も基本。
```swift
@Test(arguments: [1, 2])
func testmethod(_ n: Int) {
    #expect(n > 0)
}

// 型名を指定しても良い
@Test<[Int]>(arguments: [1, 2])
func testmethod(_ n: Int) {
    #expect(n > 0)
}
```
このとき、以下の2ケースがテストされます。
```
[1,2]
→ 1, 2
```

[Test(_:_:arguments:) | Apple Developer Documentation](https://developer.apple.com/documentation/testing/test(_:_:arguments:)-8kn7a)

## 2つの配列
2配列のすべての組み合わせがテスト対象になります。
```swift
@Test(arguments: [1, 2], ["a", "b", "c"])
func testmethod(_ n: Int, _ s: String) {
    #expect(n > 0)
    #expect(anotherInput.count == 1
}
```
この場合は6通りの組み合わせがテストされます。
```
[1,2]  [A,B,C]
→ [1,A], [1,B], [1,C], [2,A], [2,B], [2,C]
```
[Test(_:_:arguments:_:) | Apple Developer Documentation](https://developer.apple.com/documentation/testing/test(_:_:arguments:_:))

## 2つの配列のzip
zipによって作成されるタプルの配列も渡せます。
```swift
@Test(arguments: zip([1, 2], ["a", "b", "c"]))
func testmethod(_ n: Int, _ s: String) {
    #expect(n > 0)
    #expect(anotherInput.count == 1
}
```

```
[1,2]  [A,B,C]
→ [1,A], [2,B] 
```
[Test(_:_:arguments:) | Apple Developer Documentation](https://developer.apple.com/documentation/testing/test(_:_:arguments:)-3rzok)


# Traitによる挙動の変更
現時点で定義されているtrait作成メソッドは以下のとおりです。

| トレイト                 | 機能              |
| -------------------- | --------------- |
| .enabled / .disabled | 実行条件の制御         |
| .tags                | テストへのタグ付け       |
| .bug                 | issueやチケットとの紐づけ |
| .serialized          | 直列実行            |
| .timeLimit           | 制限時間（分単位）の設定    |

その他、CommentTraitなどのTraitはありますが、直接利用することはないです。


## .enable / .disable (ConditionTrait)
テストケースを有効にする/無効にする条件を設定できます。

特定の条件でテストを有効にする例。コメントはenableの条件を満たさなかったときに表示されます。
```swift
@Test(.enabled(if: AppFeatures.isCommentingEnabled))
func videoCommenting() { // ... }
```

特定の条件でテストを無効にする例。コメントはdisableの条件を満たしたときに表示されます。
```swift
@Test(.disabled(if: AppFeatures.isCommentingEnabled, "disabled"))
func videoCommenting() { // ... }
```

理由なくdisableにすることも可能です。
```swift
@Test(.disabled())
func videoCommenting() { // ... }
```


複数の条件を重ねて使う場合、すべての条件を満たしたときにテストが実施されます。コメント出力には最初に実行条件を満たさなかったTraitがskipの理由として表示されます。
```swift
@Test(
    .enabled(if: true, "condition 1"),
    .disabled(if: false, "condition 2"),
    .enabled(if: false, "condition 3"),
    .disabled(if: true, "condition 4")
)
func condition_test() {
    #expect(true)
}
    
```
→
```
​✘ Test condition_test() skipped: "condition 3"
```

### availableによる制御

Traitではないですがテストの実行条件を制御する方法として、availableもあります。

```swift
@Test
@available(macOS 15, *)
func useNewAPIs() { }
```

ただし、Traitによる制約とは異なりavailableによる制御はSuiteにつけることができません。Suiteは常に利用可能でなければならない、という制約があるためです。
```swift
@available(macOS 11.0, *) // ❌ ERROR: The suite type must always be available. 
@Suite 
struct CashRegisterTests { ... }
```

https://developer.apple.com/documentation/testing/organizingtests#Test-suite-types-must-always-be-available

## .tags (Tag)
テストにタグをつけることができます。
Xcodeのサイドバーに表示できてそこから実行もできるため、視覚できる点や、タグごとに実行ができる利点を活かしてプロジェクト共通でまとめたいテストを括れると良さそうです。
使い道として、必ずしも同じファイルにまとまるとは限らないテスト、何らかの機能単位やFlakyテストをまとめるといった使い方がありそうです。


新規のタグを定義してさらに利用する例です

```swift
extension Tag {
    @Tag static var flakyTest: Tag
    @Tag static var newFunction: Tag
}

struct Tests {
    @Test(.tags(.flakyTest, .newFunction))
    func tag_test() {
        
    }
}
```

[Tag | Apple Developer Documentation](https://developer.apple.com/documentation/testing/tag)



## .bug (BugTrait)
テストをバグissueなどと関連付けることができます。
URLを引数に取り、githubなどのissueであればリンク
このTraitがついていてもテストは実行される。.disable() や withKnownIssue とあわせて使われることが多そうです。

```swift 
@Test(.bug("https://github.com/swiftlang/swift/pull/12345"))
func bug_test() {
    withKnownIssue {
        #expect(false)
    }
}
```

[bug(_:_:) | Apple Developer Documentation](https://developer.apple.com/documentation/testing/trait/bug(_:_:))

.bugの使用例は公式ドキュメントにも記載されています
[Interpreting bug identifiers | Apple Developer Documentation](https://developer.apple.com/documentation/testing/bugidentifiers)

## .serialized (ParallelizationTrait)
並列実行を制御するためのTraitです。
テストに対して用いると、それがパラメータ化されたテストならそのテストは直列に実行されます。スレッドまで同じになるとは限りません。
以下のようなテストがあるとき、`.serialized`の有無で出力は以下のように変化します。
```swift
@Test(.serialized, arguments: [1, 2], ["a", "b", "c"])
func testArguments_2(_ input: Int, _ anotherInput: String) async throws {
    print(String(input), anotherInput, separator: ",")
    #expect(input > 0)
    #expect(anotherInput.count == 1)
}
``` 


- serializedなし（並列実行）
```
◇ Test testArguments_2(_:_:) started.
◇ Passing 2 arguments input → 1, anotherInput → "a" to testArguments_2(_:_:)
◇ Passing 2 arguments input → 2, anotherInput → "a" to testArguments_2(_:_:)
2,a
​1,a
◇ Passing 2 arguments input → 2, anotherInput → "b" to testArguments_2(_:_:)
◇ Passing 2 arguments input → 1, anotherInput → "b" to testArguments_2(_:_:)
◇ Passing 2 arguments input → 2, anotherInput → "c" to testArguments_2(_:_:)
2,c
2,b
​​1,b
◇ Passing 2 arguments input → 1, anotherInput → "c" to testArguments_2(_:_:)
​1,c
```

- serializedあり（直列実行）
```
◇ Test testArguments_2(_:_:) started.
◇ Passing 2 arguments input → 1, anotherInput → "a" to testArguments_2(_:_:)
1,a
​◇ Passing 2 arguments input → 1, anotherInput → "b" to testArguments_2(_:_:)
1,b
​◇ Passing 2 arguments input → 1, anotherInput → "c" to testArguments_2(_:_:)
1,c
​◇ Passing 2 arguments input → 2, anotherInput → "a" to testArguments_2(_:_:)
2,a
​◇ Passing 2 arguments input → 2, anotherInput → "b" to testArguments_2(_:_:)
2,b
​◇ Passing 2 arguments input → 2, anotherInput → "c" to testArguments_2(_:_:)
2,c
```

Suiteに対して用いるとそれに含まれるテスト関数が直列で実行され、さらにサブスイートがあればそれらも直列で実行されます。
```swift
@Suite(.serialized) struct FoodTruckTests { 
  @Test(arguments: Condiment.allCases) 
  func refill(condiment: Condiment) { 
 // Suiteのserializedが有効なため、このパラメタライズテストは直列で実行される
  } 
  @Test func startEngine() async throws {
   // こちらもrefillが完了されるまで呼ばれない
  }
}
```

- [ParallelizationTrait | Apple Developer Documentation](https://developer.apple.com/documentation/testing/parallelizationtrait)
- [Running tests serially or in parallel | Apple Developer Documentation](https://developer.apple.com/documentation/testing/parallelization)

## .timeLimit (TimeLimitTrait)
テストの実行で一定時間経過した場合、そのテストを失敗させるtraitです。
指定できる最小単位は「分」。
Suiteにつけると、各テストにその制限が適用されます。
```swift
@Test(.timeLimit(.minutes(1)))
func timeLimit_test() async throws {
    // 時間のかかる処理
    ...
}
```

上記のテストで制限を超えたとき、以下のようなエラーが出てテストが失敗します。
```
 Time limit was exceeded: 60.000 seconds
```

[TimeLimitTrait | Apple Developer Documentation](https://developer.apple.com/documentation/testing/timelimittrait)


# 参考
[Swift Testing | Apple Developer Documentation](https://developer.apple.com/documentation/Testing)
[Swifter and Swifty - Mastering the Swift Testing Framework | Fatbobman's Blog](https://fatbobman.com/en/posts/mastering-the-swift-testing-framework/#trait-inheritance)