【Swift】NimbleのアサーションをReactっぽく拡張してストレスを軽減する

# はじめに

SwiftのテストでNimbleを使っているのですが
React（Vitest）と比べてアサーションの書き方が少し面倒に感じ
リファクタしてみたので記事にしようと思います

# Vitest と Nimble の比較

### React (Vitest) の場合

```ts
// プリミティブ型
expect("hoge").toEqual("hoge") // もしくは toBe()
expect(10000).toEqual(10000) // もしくは toBe()

// Boolean型
expect(true).toBeTruthy()
expect(false).toBeFalsy()

// 空かどうか
expect("").toBeEmpty()

// Null型
expect(null).toBeNull()
```

### Nimble の場合

```swift
// プリミティブ型
expect("hoge").to(equal("hoge"))
expect(10000).to(equal(10000))

// Boolean型
expect(true).to(beTrue())
expect(false).to(beFalse())

// 空かどうか
expect("").to(beEmpty())

// Nil型
expect(null).to(beNil())
```

### 両者の決定的な違い

|  | メソッド | ネスト数 |
| --- | --- | --- |
| Vitest | `.toEqual()` | <font color="lime">**0**</font> |
| Nimble | `.to(equal())` | <font color="Red">**1**</font> |


Nimbleは「`.to(XXX)` で <font color="Red">**ネストしなきゃいけない**</font>」
これが、塵も積もると結構めんどいんですよね。

これを解消して、Vitestライクに書きたい。
これが今回のモチベーションです

# リファクタしよう

### 何はともあれ、まずはコード

以下のような拡張関数を作ります

```swift
@testable import my_app
import Nimble


/// for Primitive
extension SyncExpectation where Value: Equatable {
    // 同期関数用
    @discardableResult
    func toEqual(_ expectedValue: Value?) -> Self {
        return to(equal(expectedValue))
    }
    
    // 同期関数用（内部的に待つ）
    @discardableResult
    func toEventuallyEqual(_ expectedValue: Value?) -> Self {
        return toEventually(equal(expectedValue))
    }
    
    // 非同期関数用
    @discardableResult
    func toEventuallyEqual(_ expectedValue: Value?) async -> Self {
        return await toEventually(equal(expectedValue))
    }
}


/// for Boolean
extension SyncExpectation where Value == Bool {
    // 同期関数用
    @discardableResult
    func toBeTruthy() -> Self {
        return to(beTrue())
    }
    
    // 同期関数用（内部的に待つ）
    @discardableResult
    func toEventuallyTruthy() -> Self {
        return toEventually(beTrue())
    }
    
    // 非同期関数用
    @discardableResult
    func toEventuallyTruthy() async -> Self {
        return await toEventually(beTrue())
    }
    
    // 同期関数用
    @discardableResult
    func toBeFalsy() -> Self {
        return to(beFalse())
    }
    
    // 同期関数用（内部的に待つ）
    @discardableResult
    func toEventuallyFalsy() -> Self {
        return toEventually(beFalse())
    }
    
    // 非同期関数用
    @discardableResult
    func toEventuallyFalsy() async -> Self {
        return await toEventually(beFalse())
    }
}


/// for List
extension SyncExpectation where Value == [Any] {
    // 同期関数用
    @discardableResult
    func toBeEmpty() -> Self {
        return to(beEmpty())
    }
    
    // 同期関数用（内部的に待つ）
    @discardableResult
    func toEventuallyEmpty() -> Self {
        return toEventually(beEmpty())
    }
    
    // 非同期関数用
    @discardableResult
    func toEventuallyEmpty() async -> Self {
        return await toEventually(beEmpty())
    }
}


/// for Nil
extension SyncExpectation where Value: Equatable {
    // 同期関数用
    @discardableResult
    func toBeNil() -> Self {
        return to(beNil())
    }
    
    // 同期関数用（内部的に待つ）
    @discardableResult
    func toEventuallyNil() -> Self {
        return toEventually(beNil())
    }
    
    // 非同期関数用
    @discardableResult
    func toEventuallyNil() async -> Self {
        return await toEventually(beNil())
    }
}

```

プリミティブ型、Boolean型、配列型、Nil型それぞれに対して
Nimbleの`SyncExpectation`を拡張します。

それぞれ、以下のようなテスト関数のパターンに適用できるようにしています。
- 同期関数の場合
- 同期関数だけど`eventually`で何かしらの処理を待ちたい場合
- 非同期関数の場合

:::note info
**`SyncExpectation`について**
`SyncExpectation`は、Nimbleの `expect()`メソッドが返すstructの型です。
ジェネリクス`<T>`で型を一つ受け取り、それに応じてアサーションが実行されます。
要は、`expect(XXX)` 渡されたXXXによって型が決まります。

定義は以下のようになっています。
```swift
public struct SyncExpectation<Value>: Expectation {
    public let expression: Expression<Value>
    
    @discardableResult
    public func to(_ matcher: Matcher<Value>, description: String? = nil) -> Self {
        let (pass, msg) = execute(
            expression,
            .toMatch, 
            matcher, 
            to: "to",
            description: description
        )
        return verify(pass, msg)
    }
}


func expect<T>(
    fileID: String = #fileID,
    file: FileString = #filePath,
    line: UInt = #line,
    column: UInt = #column,
    _ expression: @autoclosure @escaping () throws -> T?
) -> SyncExpectation<T>
```


:::


### 使い方

プリミティブ型
```swift
func getHoge() -> String { return "hoge" }

func test_hogeは必ずhogeであること() {
    let actualHoge: String = getHoge()
    
    /// 従来の書き方
    expect(actualHoge).to(equal("hoge"))
    
    /// リファクタ後の書き方
    expect(actualHoge).toEqual("hoge")           // ✅ pass!!
    expect(actualHoge).toEventuallyEqual("hoge") // ✅ pass!!
    expect(actualHoge).toEqual("fuga")           // ❌ fail...
    expect(actualHoge).toEventuallyEqual("fuga") // ❌ fail...
}
```

Boolean型
```swift
func getTrue() -> Bool { return true }

func test_真実はいつも1つであること() {
    let actualTrue: Bool = getTrue()
    
    /// 従来の書き方
    expect(actualTrue).to(beTrue())
    
    /// リファクタ後の書き方
    expect(actualTrue).toBeTruthy()         // ✅ passed!!
    expect(actualTrue).toEventuallyTruthy() // ✅ passed!!
    expect(actualTrue).toBeFalsy()          // ❌ fail...
    expect(actualTrue).toEventuallyFalsy()  // ❌ fail...
}
```

List型
```swift
func getEmptyList() -> [String] { return [] }

func test_配列は空であること() {
    let actualEmptyList: [String] = getEmptyList()
    
    /// 従来の書き方
    expect(actualEmptyList).to(beEmpty())
    
    /// リファクタ後の書き方
    expect(actualEmptyList).toBeEmpty()          // ✅ passed!!
    expect(actualEmptyList).toEventuallyEmpty()  // ✅ passed!!
    expect(["hoge"]).toBeEmpty()                 // ❌ fail...
    expect(["hoge"]).toEventuallyEmpty()         // ❌ fail...
}
```


Nil型
```swift
func getNil() -> String? { return nil }

func test_nilはnilであること() {
    let actualNillableValue: String? = getNil()
    
    /// 従来の書き方
    expect(actualNillableValue).to(beNil())
    
    /// リファクタ後の書き方
    expect(actualNillableValue).toBeNil()         // ✅ passed!!
    expect(actualNillableValue).toEventuallyNil() // ✅ passed!!
    expect("not nill value").toBeNil()            // ❌ fail...
    expect("not nil value").toEventuallyNil()     // ❌ fail...
}
```

非同期関数の場合（プリミティブの場合を例として挙げます）

```swift
func getHoge() async -> String { return "hoge" }

func test_非同期でもhogeは必ずhogeであること() async {
    let actualHoge: String = await getHoge()
    
    await expect(actualHoge).toEventuallyEqual("hoge") // ✅ passed!!
    await expect(actualHoge).toEventuallyEqual("fuga") // ❌ fail...
}
```

# おわりに

かなりReactっぽく、且つネストすることなく
アサーションを書けるようになりました。
これで、言語・OSレベルでのコードの違いが解消され、ストレスが減りました！

今回実装し忘れましたが、`.notTo()`のパターンも拡張しておくと便利かもしれませんね。
気が向いたら更新します。