0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2025-04-13

はじめに

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

Vitest と Nimble の比較

React (Vitest) の場合

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

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

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

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

Nimble の場合

// プリミティブ型
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() 0
Nimble .to(equal()) 1

Nimbleは「.to(XXX)ネストしなきゃいけない
これが、塵も積もると結構めんどいんですよね。

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

リファクタしよう

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

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

@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で何かしらの処理を待ちたい場合
  • 非同期関数の場合

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

定義は以下のようになっています。

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>

使い方

プリミティブ型

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型

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型

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型

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...
}

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

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()のパターンも拡張しておくと便利かもしれませんね。
気が向いたら更新します。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?