こんにちは!iOS エンジニアの永田 (@ngtknt)です。今日は iOS アプリのテストにおける Fixture についてお話ししたいと思います。
Fixture とは
前もってテストデータを用意しておき、様々な Example で使い回すことでよりテストを簡単に書くことができます。このテストデータのことを Fixture と呼びます。もちろんテストデータは画像などのバイナリデータもありますし、何かしらのレスポンスデータかもしれません。
何を解決する?
今回は、Entity の Fixture について考えたいと思います。 ( Entity の定義については省略しますが、ドメインオブジェクトと考えてください) Entity の Fixture を定義すること自体はそこまで難しくないと思います。Entity を初期化して定数として定義するだけです。しかし、同時に一部のプロパティーだけ値を指定したいケースもありますよね。そういったケースにも対応したいとなると少し考える必要があります。そこで今回は以下の3点を解決する簡単な仕組みを考えたのでご紹介します。
- Entity の Fixture を提供するための Interface を揃える
- 使う側がプロパティーの値を設定せずに、適当なインスタンスを作ることができる
- 使う側がプロパティーの値を一部だけ設定しても、適当なインスタンスを作ることができる
Fixturable Protocol
まずは、一貫した Interface を作るために Protocol を作ります。Fixturable
Protocol は Self 型の static 変数 fixture
を持ちます。
protocol Fixturable {
static var fixture: Self { get }
}
例えば、以下のような User
という Entity があったとするとします。
struct User {
let id: Int
let name: String
let email: String
let profile: Profile
}
User
Entity に Fixturable
を準拠させます。ここでポイントは、すでにProfile
Entity も Fixturable
に準拠しているので .fixture
と書くだけで済む点です。
extension User: Fixturable {
static let fixture = User(
id: 1,
name: "John Smith",
email: "john@example.com",
profile: .fixture
)
}
ここまでで 1. 「Entity の Fixture を提供するための Interface を揃える」と 2. 「使う側がプロパティーの値を設定せずに、適当なインスタンスを作ることができる」が達成できました。
fixtureメソッドを自動生成
さて、続いて 3. の「使う側がプロパティーの値を一部だけ設定しても、適当なインスタンスを作ることができる」を解決したいと思います。これも実はそこまで難しくないです。先ほど作成したfixture
変数を使ってメソッドの引数のデフォルト値を設定していきます。これによって自分で指定したいプロパティーのみ指定することができ、それ以外は適当なデフォルト値で初期化されます。
extension User: Fixturable {
static func fixture(
id: Int = fixture.id,
name: String = fixture.name,
email: String = fixture.email,
profile: Profile = fixture.profile
) {
return User(
id: id,
name: name,
email: email,
profile: profile
)
}
}
以下のような呼び出し、または結果が得られます。
User()
// ▿ User
// - id : 1
// - name: "John Smith"
// - email: "john@example.com"
// ▿ profile: ...
User(name: "Kento Nagata")
// ▿ User
// - id : 1
// - name: "Kento Nagata"
// - email: "john@example.com"
// ▿ profile: ...
User(id: 360748, email: "nagata@example.com")
// ▿ User
// - id : 360748
// - name: "John Smith"
// - email: "nagata@example.com"
// ▿ profile: ...
さて、ここでお気づきでしょうが、この定義自体は機械的に行うことができます。なので、コード生成ツールである Sourcery を利用します。Sourcery は、SourceKit が生成するコードの抽象定義を利用し、テンプレートからコードを生成します。Sourcery の詳細やできることに関してはSourceryのドキュメントをご覧ください。
以下のテンプレート(swifttemplate)を書くことによって、上記のコードを自動生成できます。types.structs.filter({ $0.implements[“Fixturable”] != nil })
とあるようにFixturable
を実装した型のみを対象とします。
// fixturable.swifttemplate
// Import required module here
<%_ for type in types.structs.filter({ $0.implements[“Fixturable”] != nil }) { -%>
// MARK: - <%= type.name %> Fixturable
extension <%= type.name %> {
static func fixture(
<%_ for (index, variable) in type.storedVariables.enumerated() { -%>
<%= variable.name %>: <%= variable.typeName.name %> = fixture.<%= variable.name %><%= index == type.storedVariables.count - 1 ? “” : “,” %>
<%_ } -%>
) -> <%= type.name %> {
return <%= type.name %>(
<%_ for (index, variable) in type.storedVariables.enumerated() { -%>
<%= variable.name %>: <%= variable.name %><%= index == type.storedVariables.count - 1 ? “” : “,” %>
<%_ } -%>
)
}
}
<%_ } -%>
まとめ
さて、最後に簡単にまとめます。
- Fixturable Protocolを作ることによって Entity の Fixture に関するInterfaceを揃える
- fixture メソッドの引数のデフォルト値として fixture 変数を利用することによって、一部プロパティー値を設定可能な Fixture を取得できるようになる
- fixture メソッドは、Sourceryを利用して自動生成可能
もし同じようなユースケースでお困りの方がいればご活用ください。