[Xcode/Swift] Quick/Nimbleって何者?ポケモンで覚えるSwiftテストの基本Matcherたち

QuickはRSpec風のBDDスタイル、Nimbleはそれに使うMatcherたち

Quick/Nimbleがそもそもなんぞやという方は↓を一読ください。

[Xcode/Swift] Quick/NimbleではじめるSwiftの快適ユニットテスト入門

今回はポケモンでざっくり処理の役割を学ぶ回。

主要なmatcher

カテゴリMatcher意味使用例(概要)
値の比較equal, beGreaterThan値が一致する、大小比較expect(level).to(equal(25))
存在・空チェックbeNil, beEmpty, containnil, 空配列, 含んでいるかexpect(team).to(contain("Pikachu"))
真偽判定beTrue, beFalseBoolがtrue/falseかexpect(isLegendary).to(beTrue())
非同期toEventually, toEventuallyNot値の変化を待つexpect(x).toEventually(equal(...))
条件式(代替)to(beTrue())複雑な条件の判定expect(x > 10 && y == z).to(beTrue())

サンプルテストコード

import Quick
import Nimble
import Foundation

struct Pokemon: Equatable {
    let name: String
    let level: Int
    let type: String
    let evolution: String?
    let team: [String]
    let isLegendary: Bool
}

final class PokemonSpec: QuickSpec {
    override class func spec() {
        describe("Pokemon Matcher Examples") {
            var pikachu: Pokemon!
            var mewtwo: Pokemon!
            var emptyTeam: [String]!

            beforeEach {
                pikachu = Pokemon(
                    name: "Pikachu",
                    level: 25,
                    type: "Electric",
                    evolution: "Raichu",
                    team: ["Bulbasaur", "Charmander", "Squirtle"],
                    isLegendary: false
                )

                mewtwo = Pokemon(
                    name: "Mewtwo",
                    level: 70,
                    type: "Psychic",
                    evolution: nil,
                    team: [],
                    isLegendary: true
                )

                emptyTeam = []
            }

            // 値の比較
            context("Level Comparison") {
                it("ミュウツーのレベルはピカチュウより高い、ピカチュウのレベルは25") {
                    expect(mewtwo.level).to(beGreaterThan(pikachu.level))
                    expect(pikachu.level).to(equal(25))
                }
            }

            // 存在と空チェック
            context("Team and Evolution Checks") {
                it("nilと空をチェック") {
                    expect(mewtwo.evolution).to(beNil())
                    expect(emptyTeam).to(beEmpty())
                    expect(pikachu.team).to(contain("Bulbasaur"))
                }
            }

            // 真偽チェック
            context("Legendary Status") {
                it("ミュウツーは伝説のポケモンである、ピカチュウはそうではない") {
                    expect(mewtwo.isLegendary).to(beTrue())
                    expect(pikachu.isLegendary).to(beFalse())
                }
            }

            // 非同期チェック (簡易版)
            context("Asynchronous Test With toEventually") {
                it("ピカチュウは進化する") {
                    var evolvingPikachu: Pokemon? = nil
                    DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
                        evolvingPikachu = pikachu
                    }
                    expect(evolvingPikachu?.name).toEventually(equal("Pikachu"))
                    expect(evolvingPikachu?.evolution).toEventuallyNot(beNil())
                }
            }

            // 条件チェック & 同一インスタンス
            context("Advanced Matchers") {
                it("特定のConditionを満たすピカチュウ") {
                    expect(pikachu.level >= 20 && pikachu.type == "Electric").to(beTrue())
                }

                it("同一インスタンスのチェック") {
                    let sameMewTwo = mewtwo
                    expect(mewtwo).to(equal(sameMewTwo))
                }
            }
        }
    }
}

SwiftLint の nimble_operator 警告がでたら(→ offでもいい or 演算子使ってもOK)

まとめ:テストは「難しさ」より「慣れ」

  • Matcherが分かれば、テストコードは「読みやすく、書きやすく」なる
  • 今回のような感覚で覚えれば、怖くないし、テスト文化も育つ
  • 次のステップ:実アプリで使う、↓が少し参考になるかも