17
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Gopher道場Advent Calendar 2018

Day 16

Goのテストでseedingする

Last updated at Posted at 2018-12-16

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が標準で用意しているFileMigrationSourceHttpFileSystemMigrationSourceMemoryMigrationSourceなどの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 さんのエントリになります。お楽しみに!

17
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?