DBスキーマからKotlinのテストフィクスチャを自動生成するgradleプラグインを作った

サーバーサイドKotlinでDB接続テストする際、テストデータのセットアップにはDbSetup が便利です。DbSetupは「xmlなどの外部ファイル」ではなく「コード」でテストフィクスチャを生成できるJavaライブラリで、以下のようなKotlin用のDSLも提供してくれているので重宝しています。

    insertInto("users") {
        mappedValues(
                "id" to 1,
                "name" to "前原 秀徳",
                "job" to "engineer",
                "status" to "ACTIVE",
                ...
        )
    }

ただ、これだとカラム数が数十個になってくると記述が面倒だし(仕事だと100カラム近いテーブルもあるんですよね。。)、ちょっとだけ値が異なるパターンを色々作りたい、といった際にしんどいな感じてました。

DBスキーマからコードを自動生成してくれて、かつRubyのfactory_botのような使い心地だったら楽だなと思い、factlinというgradle pluginを作ったので紹介します。

github.com

  • 既存のDBスキーマを元にKotlinのコードを自動生成する(自動生成されたコードはDbSetupに依存)
  • 自動生成されたコードを使うとRubyのfactory_botのような使い心地でテストフィクスチャをセットアップできる
  • 自動生成されるコードのテンプレートやデフォルト値はカスタマイズ可能

といった代物です。例えば、postgresでこんなテーブル定義があった場合

 CREATE TABLE users (
  id SERIAL PRIMARY KEY,
  name VARCHAR(256) NOT NULL,
  job VARCHAR(256) NOT NULL DEFAULT 'engineer',
  status VARCHAR(256) NOT NULL DEFAULT 'ACTIVE',
  age INTEGER NOT NULL,
  score NUMERIC NOT NULL,
  is_admin BOOLEAN NOT NULL,
  birth_day DATE NOT NULL,
  nick_name VARCHAR(256),
  created_timestamp TIMESTAMP WITHOUT TIME ZONE NOT NULL,
  updated_timestamp TIMESTAMP WITHOUT TIME ZONE
);

COMMENT ON TABLE users IS 'user table';
COMMENT ON COLUMN users.id IS 'primary key';
COMMENT ON COLUMN users.name IS 'user name';
COMMENT ON COLUMN users.job IS 'job name';
COMMENT ON COLUMN users.status IS 'activate status';
COMMENT ON COLUMN users.age IS 'user age';
COMMENT ON COLUMN users.score IS 'game score';
COMMENT ON COLUMN users.is_admin IS 'user is admin user or not';
COMMENT ON COLUMN users.birth_day IS 'user birth day';
COMMENT ON COLUMN users.nick_name IS 'nick name';

このプラグインを実行すると、以下のようなKotlinのコードが自動生成されます。

data class UsersFixture (
    val id: Int = 0, // primary key
    val name: String = "", // user name
    val job: String = "", // job name
    val status: String = "", // activate status
    val age: Int = 0, // user age
    val score: BigDecimal = 0.toBigDecimal(), // game score
    val is_admin: Boolean = false, // user is admin user or not
    val birth_day: LocalDate = LocalDate.now(), // user birth day
    val nick_name: String? = null, // nick name
    val created_timestamp: LocalDateTime = LocalDateTime.now(), 
    val updated_timestamp: LocalDateTime? = null 
)

fun DbSetupBuilder.insertUsersFixture(f: UsersFixture) {
    insertInto("users") {
        mappedValues(
                "id" to f.id,
                "name" to f.name,
                "job" to f.job,
                "status" to f.status,
                "age" to f.age,
                "score" to f.score,
                "is_admin" to f.is_admin,
                "birth_day" to f.birth_day,
                "nick_name" to f.nick_name,
                "created_timestamp" to f.created_timestamp,
                "updated_timestamp" to f.updated_timestamp
        )
    }
}

生成されたコードは、

  1. テストフィクスチャを表すData Class
  2. DbSetupのinsertIntoメソッドをラップした拡張関数

で構成されてます。1.Data Classの各プロパティには、データベースのカラム型に応じたKotlinの型とデフォルト値が自動生成されます。デフォルト値はnullableなカラムならnull、nullableでない場合はカラムの型に応じた値(数値なら0、文字列なら空文字)といった具合に設定されますが、後述するカスタマイズ設定である程度カスタマイズ可能です。また、DBカラムにコメントがついている場合、コメントもつくようになっています。

上記の自動生成されたコードは、テストの中で以下のように使います。これによりDBにテストデータがinsertされ、そのデータを用いたテストを行うことができます。

dbSetup(dest) {
    deleteAllFrom(listOf("users")) 
    // DBにテストデータをセットアップする
    insertUsersFixture(UsersFixture(id = 1, name = "foo"))
    insertUsersFixture(UsersFixture(id = 2, name = "bar"))
}.launch()

...DBのデータを使ったテスト

自動生成されたFixtureクラスはData Classになっており、かつカラムの型に沿ったデフォルト値が設定されているので、「ちょっとだけ値が異なるテストフィクスチャ」を簡単に生成することができます。IDEで補完が効いてくれて、定義元にジャンプすればカラムのコメントも確認できて便利です(100個近くカラムがあるテーブルだと特に嬉しい)

f:id:maeharin:20180809082049g:plain

フィクスチャのパターンを作る

自動生成されたクラスがData Classになっているという点がポイントで、Data Classのcopyメソッドを使うことで、Rubyのfactory_botのように「フィクスチャのパターンに名前をつけて取り回せるようにしておく」ことが可能です。以下のようにKotlinの拡張関数とcopyを使います。ちなみに、メンテナンスしやすくするため(カラムが変更されコードを再生成する場合に備えて)、自動生成されたFixtureクラスには手を加えず、別のディレクトリとファイルを作って拡張関数を定義するのがオススメです。

fun UsersFixture.default() = this.copy(job = "エンジニア", age = 30)
fun UsersFixture.active() = this.default().copy(status = "ACTIVE")
fun UsersFixture.inActive() = this.default().copy(status = "IN_ACTIVE")

テストでこんな風に使います

insertUsersFixture(UsersFixture().active().copy(id = 1, name = "テスト1"))
insertUsersFixture(UsersFixture().inActive().copy(id = 2, name = "テスト2"))

カスタマイズ

自動生成されるコードはある程度カスタマイズ可能となっています。

設定 内容 デフォルト
fixtureOutputDir 自動生成されるコードの出力先ディレクト src/test/kotlin/com/maeharin/factlin/fixtures
fixturePackageName 自動生成されるコードのパッケージ名 com.maeharin.factlin.fixtures
fixtureTemplatePath 自動生成に使うテンプレート(FreeMarker) のパスです デフォルト
exclude table names 自動生成の対象外にするテーブルリスト なし
includeTables 自動生成の対象にするテーブルリスト すべて
cleanOutputDir trueにすると自動生成するまえに出力先ディレクトリを削除します(メンテナンス性のためにtrueにすることを推奨) false
customDefaultValues 特定のテーブルの特定のカラムのデフォルト値を上書き。形式: [tableName, columnName, defaultValue] なし
customTypeMapper データベースの型をKotlinの型に変換するルールを上書き。形式: [databaseColumnType, KotlinType] デフォルトルール

リポジトリのREADMEに設定例があるのでご参照ください。

ぜひ使ってみてください!

この仕組でこれまで1000ケース以上テストケースを書いてきましたが、とくに問題なく快適に使えています。もしよければ、ぜひ使ってみてください!

(宣伝)Kotlin Fest 2018に登壇します

2018年8月25日(土)に東京コンファレンスセンター品川にて開催されるKotlin Fest 2018に登壇します。サーバーサイドKotlinのテストに関して話す予定ですので、ぜひご来場ください!

kotlin-fest-2018.peatix.com