Gopher道場 Advent Calendar 16日目のエントリです。
昨日は @kaznishi さんによる[豆知識]Bounds Check Eliminationでした。
Database周りの実装はテストでモックせずに、ちゃんとDBを使ってseedingしてテストしたくなります。
rubenv/sql-migrateを使って、migrationとseedingをいい感じに共存させる方法をご紹介します。
ちなみにdatastoreの場合はtimakin/dsmockが便利です。
ディレクトリ構造
まずmigrationとseedingをディレクトリ別々で作り、SQLを書いていきます。
他の言語のフレームワークを触ってるとよく出会う構成ですね。
./sql
├── migrations
│ └── 1.sql
└── testdata
└── 1_test.sql
migrationをしたいときはmigrations
だけを、migrationとseedingを両方やるときは両方のディレクトリをがっちゃんこすればいい、という考え方です。
実装
Go製のmigrationツールはいくつかありますが、rubenv/sql-migrateを選んだ理由はmigrationファイルを柔軟にわたすことができるからです。
こちらがmigrationを実行する関数です。
// Execute a set of migrations
//
// Returns the number of applied migrations.
func Exec(db *sql.DB, dialect string, m MigrationSource, dir MigrationDirection) (int, error) {
return ExecMax(db, dialect, m, dir, 0)
}
MigrationSource
はInterfaceとなっていて、FindMigrations()
が実装されていればよい。
type MigrationSource interface {
// Finds the migrations.
//
// The resulting slice of migrations should be sorted by Id.
FindMigrations() ([]*Migration, error)
}
ちなみにrubenv/sql-migrateが標準で用意しているFileMigrationSource
、HttpFileSystemMigrationSource
、MemoryMigrationSource
などのstructたちはFindMigrations()
の振る舞いを持っています。
ということは、複数のFindMigrations()
をがっちゃんこするFindMigrations()
を実装すれば、複数ディレクトリを指定できるはず・・・!
type migrationSources struct {
sources []migrate.MigrationSource
}
func (mss *migrationSources) FindMigrations() ([]*migrate.Migration, error) {
var migrations []*migrate.Migration
for _, s := range mss.sources {
ms, err := s.FindMigrations()
if err != nil {
return nil, err
}
migrations = append(migrations, ms...)
}
return migrations, nil
}
migrationSources
に./sql/migrations
と./sql/testdata
が合わさってmigrationが走ります。
mc := &migrationSources{
sources: []migrate.MigrationSource{
&migrate.FileMigrationSource{
Dir: "./sql/migrations",
},
&migrate.FileMigrationSource{
Dir: "./sql/testdata",
},
},
}
_, err = migrate.Exec(db, "postgres", mc, migrate.Up)
これをdatabaseを使うテストに挟んであげると、ケースごとにまっさらな状態にテストできます。
参照系のテストであれば、ケースごとではなくていいと思います。
for name, c := range cases {
t.Run(name, func(t *testing.T) {
migrateUp(t)
defer migrateDown(t)
// ...
})
}
デメリット
DBのmigrationテーブルを見てみると、本来入るべきではないseedingのファイルが入ってしまいます。
が、テスト時だけなら、毎回、migrationをゼロから実行するので問題はなさそうです。
SELECT * FROM gorp_migrations;
id | applied_at
------------+-------------------------------
1.sql | 2018-12-12 14:00:05.608612+00
1_test.sql | 2018-12-12 14:00:05.610527+00
まとめ
rubenv/sql-migrateはmigrationライブラリながらも工夫をすれば、テスト時のseedingにも使える。ただし、seedingのファイル名もmigrationテーブルに記録されてしまうので、アプリケーションを動かすときのseedingには向かない。
サンプルコードはこちら
https://github.com/sawadashota/sql-test-sample
明日は @mizkei さんのエントリになります。お楽しみに!