Swift Testingと仲良くなる


C、KotlinやRustは割とすんなりテストコードが書けるものの、なぜかSwift（XCode）だけ敷居が高いと感じていた`XCTest`。気づいたら新しく`Swift Testing`がリリースされていたので、今回は`Swift Testing`と仲良くなろうと思い、やったことなどをまとめてみました。

## 公式サイト

https://developer.apple.com/documentation/xcode/testing


`XCTest`から`Swift Test`にマイグレーションするときは以下のドキュメントを参考にしてください。
https://developer.apple.com/documentation/testing/migratingfromxctest

# 単体テストの基本形
`XCTest`から`Swift Test`に変わり、評価関数が`#expect`と`#require`のみかなりシンプルになりました。テストのショートカットキーが`⌘U`なので `⌘Command  u` でテストが実行できます。


### テストコード
```swift
import Testing

@Suite("Swift Test Demo")  struct SwiftTestDemo {
    @Test("Simple test") func example() {
        #expect(1 == 1)
    }
}
```

## 成功パターン
```
􀟈 Test run started.
􀄵 Testing Library Version: 102 (x86_64-apple-macos13.0)
􀟈 Suite "Swift Test Demo" started.
􀟈 Test "Simple test" started.
􁁛 Test "Simple test" passed after 0.001 seconds.
􁁛 Suite "Swift Test Demo" passed after 0.001 seconds.
􁁛 Test run with 1 test passed after 0.001 seconds.
```

## 失敗パターン
XCodeの画面でビルドエラーと同じように`#expect`で失敗した箇所が赤く表示されます。


# テストコードを詳しくみてみる
基本形のコードを１行ずつ詳しくみてみましょう。

## `Swift Testing`のロード
`XCTest`から`Testing`に変わっただけです。

```swift
import Testing
```

## `@Suite`マクロ
テストスイートとしてまとめるマクロです。

```swift
@Suite("Swift Test Demo")  struct SwiftTestDemo {
    // ...
}
```

`@Suite`マクロは、引数を２つ以上設定することができます。`@Test`が`struct`,`actor`,`class`内に含まれる場合は、明示する必要はありません。引数に`displayName`を設定したり、`SuiteTrait`を指定したいときはマクロを明記する必要があります。


### 定義
```swift
@attached(member) @attached(peer)
macro Suite(
    _ displayName: String? = nil,
    _ traits: any SuiteTrait...
)
```

### `SuiteTrait`について
端的に言うと、テストに対して条件を加えたり補足情報を加えたりできるTraitになります。種類は以下の通りです。

|Topics|利用方法|
|---|---|
|`.enabled`|テストの有効化|
|`.disabled`|テストの有効化|
|`.timeLimit`|実行時間の制限を加える|
|`.serializes`|テストの実行を連続・並列化する|
|`.tags`|テストに対してタグ情報を加える|
|`.bug`|テストにバグ情報（Issueなど）を追加する|
|`.comment`|テストにコメントを加える|
|`.prepare`|事前にテスト合格が必要なテストを関連付ける|


## `@Test`マクロ

`@Test`はテスト対象であることを明示するマクロです。テスト実施に必須のマクロです。
```swift
@Test("Simple test") func example() {
    // ...
}
```

### 定義
```swift
@attached(peer)
macro Test(
    _ displayName: String? = nil,
    _ traits: any TestTrait...
)
```

### `TestTrait`について
`Trait`プロトコルを継承しているため、`SuiteTrait`と同様に`Trait`が利用可能です。テストに対して条件を加えたり補足情報を加えたりできるTraitになります。種類は以下の通りです。

|Topics|利用方法|
|---|---|
|`.enabled`|テストの有効化|
|`.disabled`|テストの有効化|
|`.timeLimit`|実行時間の制限を加える|
|`.serializes`|テストの実行を連続・並列化する|
|`.tags`|テストに対してタグ情報を加える|
|`.bug`|テストにバグ情報（Issueなど）を追加する|
|`.comment`|テストにコメントを加える|
|`.prepare`|事前にテスト合格が必要なテストを関連付ける|



#### リファレンス
[https://developer.apple.com/documentation/testing/test(_:_:arguments:)-3rzok](https://developer.apple.com/documentation/testing/test(_:_:arguments:)-3rzok)


### `#expect`マクロ
かなりシンプルです。
```
    #expect(1 == 1)
```


### XCTestとSwift Testの評価関数対応表
https://developer.apple.com/documentation/testing/migratingfromxctest#Record-issues



# 色々とテストパターンを考えてみる

## 1.オブジェクト共通化
クラスのテストコードを書く場合、毎回生成と破棄を繰り返さなくてもよい場合は`setup`と`teardown`を活用して、オブジェクト共通化をすることでテスト実施の高速化が期待できます。

```swift:Calculator.swift
class Calculator {
    func add(_ x: Int, _ y: Int) -> Int {
        return x  y
    }
}
```

```swift:CalculatorTest.swift
@Suite struct CalculatorTest {
    // オブジェクト共通化
    fileprivate let calc: Calculator;
    
    // setup関数相当
    init() {
        self.calc = .init()
    }

    @Test func addTest() {
        let expected: Int = 7;
        let actual = calc.add(2, 5)
        
        #expect(actual == expected)
    }

    @Test func addMinusValueTest() {
        let expected: Int = -2;
        let actual = calc.add(-5, 3)
        
        #expect(actual == expected)
    }
}
```

## 2.テストに実施な要素が揃っているか事前にチェックする (`#require`)
`try #require()`を使って、検査実施前に要素が揃っているかチェックします。

```swift:CalculatorTest.swift
@Suite struct CalculatorTest {
    // オブジェクト共通化
    fileprivate let calc: Calculator;
    
    // setup関数相当
    init() {
        self.calc = .init()
    }

    @Test func addTest() throws {
        let expected: Int = 7;
        let actual = calc.add(2, 5)
        
       try #require(calc != nil)
        #expect(actual == expected)
    }

    // テスト失敗のケース
    @Test func addTestExpectsTestFailed() throws {
        let calc : Calculator? = nil
        let expected: Int = 7;
        
        try #require(calc != nil)
        
        let actual = calc!.add(2, 5)
        #expect(actual == expected)
    }

}
```


## 3.期待する例外が発生したかチェックする
エラー系のテストケースです。`#expect`を使う点は変わりません。

```swift:Calculator.swift
class Calculator {
    func add(_ x: Int, _ y: Int) -> Int {
        return x  y
    }
    
    func divide(_ numerator: Int, _ denominator: Int) throws -> Double {
        if denominator == 0 {
            throw CalculatorError.NotDividedByZero;
        }
        
        return Double((numerator/denominator));
    }
}
```


```swift:CalculatorError.swift
import Foundation

enum CalculatorError: LocalizedError {
    case Unknown
    case NotDividedByZero
    
    var errorDescription: String? {
        switch self {
            case .Unknown: return "Unknown error happened"
            case .NotDividedByZero: return "Not devided by Zero"
        }
    }
}
```

```swift:CalculatorTest.swift
@Suite struct CalculatorTest {
    // オブジェクト共通化
    fileprivate let calc: Calculator;
    
    // setup関数相当
    init() {
        self.calc = .init()
    }

    // 何かしらのErrorが発生したか検査する
    @Test func throwAnyCalculatorErrorTest() throws {
        #expect(throws: CalculatorError.self) {
                  try calc.divide(2, 0)
        }
    }
    
    // NotDividedByZeroエラーがきちんとが投げられたか検査する
    @Test func throwDividedByZeroErrorTest() throws {
        #expect(throws: CalculatorError.NotDividedByZero) {
                  try calc.divide(2, 0)
        }
    }

    // 期待するErrorと異なるエラーがが投げられたか検査する
    @Test func throwMissingErrorTest() throws {
        #expect(throws: CalculatorError.Unknown) {
                  try calc.divide(2, 0)
        }
    }
}
```