僕の携わるアプリケーションの開発では、テストデータを Test Data Builder Pattern を用いて生成しています。データクラスと対になるように Builder クラスを実装するのですが、この Builder 実装を自動化したいと思います。
まず最初に、少しだけ Test Data Builder Pattern について触れてみます。
Test Data Builder Pattern
Test Data Builder Pattern は、Unit テストなどのテストデータを Builder Pattern を利用して生成する方法です。データクラスと対になるように Builder クラスを実装し、テストデータの生成は Builder クラスを通して行うようにします。
data class User(val id: Int, val name: String)
class UserBuilder(private var id: Int = 0, private var name: String = "Atsushi") {
fun build() = User(this.id, this.name)
fun withId(id: Int): UserBuilder {
this.id = id
return this
}
fun withName(name: String): UserBuilder {
this.name = name
}
}
val testUser = UserBuilder().withId(1).build()
この方法でテストデータを生成するメリットは
- コンストラクタ呼び出しによるデータ生成が乱立しない
- 必要なパターンに応じたテストデータの組み立てが容易になる
などが挙げられます。生成処理が乱立せず一箇所に集約できるため、データクラスの構造変更に強くなります。(データクラス構造の変更可能性が少ない場合は特に採用の必要は無さそうです)
一方で、データクラスが多くなってきたりプロパティを多く持つデータクラスの Builder クラス実装は、かなりコストが掛かります。
Builder クラス自動生成
データクラスと Builder クラスは、「同一のプロパティを持つ」「build メソッドを持つ」「with メソッドを持つ」といったシンプルな関係になっています。この条件をもとに Builder クラスを自動生成できれば、データクラスが多くなってきたりプロパティを多く持つ場合でも問題無く Builder クラスを作れそうです。
Builder クラスの自動生成には BuilderKit を利用します。Kotlin 用の Builder クラス自動生成ライブラリです。先程の User クラスを参考にします。
まずは プロジェクトに BuilderKit を追加して下さい。
<dependency>
<groupId>com.github.atsushi130</groupId>
<artifactId>builderkit</artifactId>
<version>0.5.0</version>
</dependency>
次に自動生成クラスの書き出しディレクトリを作成します。プロジェクトのルートディレクトリに generate/src
を作成して下さい。(書き出し先はデフォルトで generate/src
ですが指定することもできます)
❯ mkdir generate/src
最後に src/gen/kotlin
などに Generator.kt
を作成して下さい。ここで自動生成対象のクラスを定義していきます。
import builderkit.BuilderGenerator
class Generator {
companion object {
@JvmStatic fun main(vararg args: String) {
BuilderGenerator.generates(User::class)
}
}
}
BuilderGenerator#generates
は可変長引数で KClass を取るので複数のクラスを指定することができます。設定後に main メソッドを実行すると generate/src
にデータクラスに対応した Builder クラスが生成されます。
自動生成される UserBuilder の形式は以下のようになります
class UserBuilder(private var id: Int, private var name: String) {
fun build() = User(this.id, this.name)
fun withId(id: Int): UserBuilder {
this.id = id
return this
}
fun withName(name: String): UserBuilder {
this.name = name
return this
}
}
自動生成後の Builder クラスは Primary Constructor のデフォルト引数が未設定の状態なので、任意の値を設定して下さい。
適切な package に移動すればテストデータ生成の準備完了です!
終わりに
仕様変更や修正に弱いテストコードを、実装コストを抑えつつ少しでも辛さを軽減できるようにして行きたいです。BuilderKit への PR もお待ちしています