テスト時のみEquatableプロトコルに準拠させる方法(Swift)


はじめに

「製品コードでは構造体を比較しないけど、テスト時は比較したい」ということはありませんか

Equatable プロトコルに準拠することで、構造体同士を == で比較できるようになります。

FooEntity.swift
struct FooEntity: Equatable {
    let name: String
    let age: Int
}
FooTests.swift
let fooEntity1 = FooEntity(name: "foo", age: 18)
let fooEntity2 = FooEntity(name: "foo", age: 24)

// `Equatable` に準拠しているため、ビルドが通る
XCTAssertEqual(fooEntity1, fooEntity2) // `age` が異なるのでテストは失敗する

このように製品コードで直接 Equatable プロトコルに準拠させる方法が考えられますが、「 テスト時のみ比較したい 」という意図が読み取れません。

Twitterでテスト時のみ Equatable プロトコルに準拠させる方法を教えていただいたので、備忘録として残します。

個人的な結論:テスト時のみEquatableプロトコルに準拠させる必要はない

本題に入る前にまず私の結論を言います。
本末転倒ですが、Twitterでいろいろ意見を頂いた結果、私は製品コードで Equatable プロトコルに準拠させればいいと結論付けました。
理由は以下の通りです。

  • Equatable プロトコルに準拠しても副作用がほとんどない
    • コンパイラが自動生成する == のコードによるオーバーヘッドくらい
  • テスト時のみ Equtable プロトコルに準拠させる方法はどれもデメリットがある
    • 詳しくは後述する
  • 「構造体が比較できる」ことを当たり前の言語仕様と考えれば、製品に不要なコードが含まれていることにはならない
    • StringやIntも Equatable プロトコルに準拠しており、比較しなくても普通に使っている

方法①:テスト用のマクロを作る

@_ha1f さんから教えていただきました。

テスト用のマクロを作り、テスト時のみエクステンションで準拠させます。

FooEntity.swift
struct FooEntity {
    let name: String
    let age: Int
}

#if TEST
extension FooEntity: Equatable {}
#endif

これは思いつかなかったので目から鱗でした。
「テスト時のみ比較する」という意図が明確にわかります。

ただし、「 マクロが増える 」というデメリットがあります。

方法②:テストターゲットで準拠させる

方法①のエクステンションをテストターゲット側に記述すれば、マクロが不要になります。

FooTests.swift
@testable import FooTarget

extension FooEntity: Equatable {
    public static func == (lhs: FooEntity, rhs: FooEntity) -> Bool {
        return lhs.name == rhs.name && lhs.age == rhs.age
    }
}

しかし、 == のコードを自分で実装し、 public にする必要があります。
(なぜその必要があるかの言語仕様までは調べていません)

これは「 構造体に新しくプロパティを追加したときに、修正が必要になる 」というデメリットがあります。
修正しなくても警告やエラーが発生しないため、忘れがちになります。

おわりに

方法①も②もデメリットがあることがわかりました。
私はできる限りテスト時のみ使うコードを製品に含めたくないので、デメリットがなければ採用していました。

Twitterで議論が広がると、自分だけでは考えつかなかったことを学べるので、とてもありがたいです。
他にご意見のある方がいらっしゃれば、遠慮なくコメントやTwitterでご連絡ください!