5
7

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.

Go のテストで Spanner を使う

Last updated at Posted at 2018-07-09

現時点では Spanner のエミュレータが提供されていないので、e2e テストやレポジトリレベルのテストを書く際は実際に Spanner に接続してテストしており、その覚書がこちら。
また Abort など Spanner を叩いて初めて得られる問題もあるのでエミュレータ提供されてもやはり実際の Spanner を使ったテストは必要そう。

TL;DR

手元でテストする際は開発者ごとに専用のデータベースを払い出しておき、それを使う。
スキーマは必要に応じて開発者自身がリセットする。

テストコードで作成したデータは、tearDown の処理で全削除する。

CI でテストする際も同様だが、データベース作成の時間を短縮したい場合はスキーマのハッシュとデータベースを紐付け払い出すような管理ツールを仕込んでおくと良い。

セットアップ


func (c *AdminClient) CreateDatabaseWithStatement(ctx context.Context, name string, statements []string) error {
	operation, err := c.admin.CreateDatabase(ctx, &admindbpb.CreateDatabaseRequest{
		Parent:          c.config.Parent(),
		CreateStatement: fmt.Sprintf("CREATE DATABASE `%s`", name),
		ExtraStatements: statements,
	})
	if err != nil {
		return err
	}
	if _, err := operation.Wait(ctx); err != nil {
		return err
	}

	return nil
}

CreateDatabase の CreateStatement に DDL を渡してデータベースと同時にテーブル作成も行う。ここを個別の RPC コールでやると待ち時間が増加するので同時にやる。

tear down に使う truncate を用意する。

func (c *AdminClient) Truncate(ctx context.Context) error {
	var m []*spanner.Mutation

	for _, t := range Tables {
		m = append(m, spanner.Delete(t, spanner.AllKeys()))
	}
	_, err := c.client.ReadWriteTransaction(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
		return txn.BufferWrite(m)
	})
	if err != nil {
		return err
	}

	return nil
}
defer require.NoError(t, client.admindb.Truncate())

テストで必要となるデータを map で定義して突っ込めるような関数もあると便利。

func MustCreateMutation(table string, structs interface{}) []*spanner.Mutation {
	var ms []*spanner.Mutation
	switch k := reflect.TypeOf(structs).Kind(); k {
	case reflect.Map:
		mrv := reflect.ValueOf(structs)
		for _, k := range mrv.MapKeys() {
			v := mrv.MapIndex(k)
			m, err := spanner.InsertStruct(table, v.Interface())
			if err != nil {
				panic(err)
			}
			ms = append(ms, m)
		}
	case reflect.Slice:
		rv := reflect.ValueOf(structs)
		for i := 0; i < rv.Len(); i++ {
			m, err := spanner.InsertStruct(table, rv.Index(i).Interface())
			if err != nil {
				panic(err)
			}
			ms = append(ms, m)
		}
	default:
		msg := fmt.Sprintf("unsupported type: %s", k)
		panic(msg)
	}
	return ms
}

クレデンシャル

ローカルでは個人の Application Default Credential を使う。

CI 環境ではサービスアカウントを使う。
発行した Service Account のクレデンシャルを CI にシークレットとして埋め込んでおき実行時に渡す。

  environment:
      GOOGLE_APPLICATION_CREDENTIALS: /etc/google/application-credentials.json
# ..略..
  - &set_project_credential
    name: Set Project Credential
    command: |
      mkdir -p /etc/google/
      echo ${DEV_CREDENTIALS} > ${GOOGLE_APPLICATION_CREDENTIALS}

おまけ

os.Exit と defer

os.Exit が呼ばれると defer が実行される前に終了する。
次のようにすると回避できる。

func TestMain(m *testing.M) {
	// ...
	os.Exit(func() int {
		defer func() {
			// here's termination processes
		}()
		return m.Run()
	}())
}

並列実行とデータの食い合い

テストはパッケージ毎に並列実行されるので複数で TestMain を呼んで Spanner に書き込み、削除を行うような規模にテストが広がると並列で実行されるテストがそれぞれ同時にデータを書き込み / 削除し始め、動作が壊れ始める。

-p 1 にしてシリアルに実行させると現象が収まるので確認に使える。
実際の回避方法として現実的なのはデータベースを分ける、データベースへの読み書きを必要とするテストはひとつ所に押し込めて単一パッケージ内でテストを行う、といったのもになる。

5
7
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
5
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?