Nimbleでenum(Associated Value)の自作matcher作って可読性を上げる

Nimbleでenum(Associated Value)の自作matcher作って可読性を上げる

Table of Contents

テストフレームワークでQuickとマッチャーにNimbleを使って開発する人は多いのではないかと思います。

通常のenumであればテストは別に困ることはないのですが、それがAssociated Valueとなると話が180度変わります。 そのまま対策せずゴリ押しでテストコードを書くと、後で気持ち悪いテストコードを目にすることになるので、 少しでも健全で見やすいテストコードを書く方法について書いています。

テストターゲットは次のモデル

次のようなAssociated Valueなenumがあったとします。

enum VariableType {
    case string(String)
    case integer(Int)
    case float(Float)
    case boolean(Bool)
}

特に何もしないでテストを書いた場合

この状態のままテストをする場合、次のようなコードになると思います。

VariableTypeSpec.swift

class VariableTypeSpec: QuickSpec {
    override func spec() {
        describe("VariableTypeSpec") {
            it("") {              
                let value = VariableType.string("Hoge")
                if case let VariableType.string(string) = value {
                    expect(string) == "Hoge"
                } else {
                    fail()
                }
            }
        }
    }
}

Equatable protocolを採用する

これを解決するには, Equatable protocol を VariableType に採用することでよりシンプルな書き方が可能になります。

VariableType.swift

extension VariableType: Equatable {
    static func == (lhs: VariableType, rhs: VariableType) -> Bool {
        switch (lhs, rhs) {
        case let (.string(left), .string(right)): return left == right
        case let (.integer(left), .integer(right)): return left == right
        case let (.float(left), .float(right)): return left == right
        case let (.boolean(left), .boolean(right)): return left == right
        default: return false
        }
    }
}

VariableTypeSpec.swift

class VariableTypeSpec: QuickSpec {
    override func spec() {
        describe("VariableTypeSpec") {
            it("") {
                expect(VariableType.string("Hoge")) == .string("Hoge")
            }
        }
    }
}

Custom Matcher を用意する

今のままで十分可読性は良いのですが、もし Equatable だけでは厳しい場合やエラー時のメッセージの情報を増やしたい場合は Custom Matcherを用意するといいでしょう。

func message(expected: VariableType) -> Predicate<VariableType> {
    return Predicate { (expression: Expression<VariableType>) throws -> PredicateResult in
        let message = ExpectationMessage.expectedActualValueTo("message \(expected)")
        if let actual = try expression.evaluate() {
            return PredicateResult(bool: actual == expected, message: message)
        } else {
            return PredicateResult(status: .fail, message: message)
        }
    }
}

このようにルールに準拠したメソッドを用意することで次のようにNimbleでMatcherとして使えるようになります。

expect(VariableType.string("Hoge")).to(message(expected: .string("Hoge")))

まぁこれは equal() とほぼ同じMatcherになりますね。

テストコードをプロダクションに含む是非について

私は追加することは構わないと思います。

なぜならSwiftのMockingはマニュアルモッキングを推奨していますが、
マニュアルモッキング用にprotocolを用意してMockを作ることが、既にテストコードが設計レベルで組み込まれているからです。

本来ならprotocolを挟まずともよい部分にも関わらずprotocol化したり、DIで依存性注入したりと、 「なんのためにその実装を書いているのか?」と考えればテスタビリティを上げるためです。

であれば、今更テストコードがプロダクションに入ることになんの違和感を持ちましょうか。

違和感を感じる暇があれば、テストコードの暴発と本番時のみ動くコードを減らす労力に回したほうがマシです。

これは仕方がないことです。Swiftという言語仕様がテスタビリティを下げているので従うしかなく、
私達が求めるテスタビリティまで向上させるには必要せざる得ない対応策だと思います。

このエントリーをはてなブックマークに追加