✔️

GORMとgo-txdbで、DBを使ったユニットテストを安全に実行する

2024/09/25に公開

この記事でわかること

go-txdbを使ってgormでDBの読み書きを行うテストを安全に実行できるようにします。

概要

DB読み書きを伴う自動テストを記述するとき、あるテストが別のテストで作成されたテストデータの影響を受けてテストが失敗してしまうことがあります。

go-txdbを使うと、テストケース(コネクション)ごとにテストデータを分離できるので、テストケース間でテストデータが汚染されることを気にする必要がなくなったり、テスト終了後にテストデータの削除を気にする必要がなくなります。

テスト用にインメモリのsqliteを使用します。
本来はMySQLやPostgreSQLのように永続的にインスタンスが存在するDBで利用するとメリットがわかりやすいです。

登場するライブラリ

  • gorm
    • golangのORM
  • go-txdb
    • テストケースごとにトランザクションを分離して実行するSQLドライバ

コード

https://github.com/kuwa72/sample-gorm-txdb-testing

ポイントだけ書きます。
gormインスタンスを作成する前にtxdb.Registerを呼び出しておく。
テストごとにコネクションを作成してテストを実行する。
nameはテストデータを分離してほしい単位(今回はテストの関数ごと)でユニークにしておく。

sqlite.NewのDriverNameとしてnameを渡すとtxdb経由でsqliteが呼び出されるようになる。
MySQL/PostgreSQLでも同様にするとテストがトランザクション内で実行される。

func NewTestDB(name string) (*gorm.DB, error) {
	txdb.Register(name, "sqlite3", dsn)
	dialector := sqlite.New(sqlite.Config{
		DriverName: name,
		DSN:        dsn,
	})

	db, err := gorm.Open(dialector, &gorm.Config{
		Logger: logger.Default.LogMode(logger.Info),
	})
	if err != nil {
		return nil, err
	}
	initDB(db)
	return db, nil
}

各テストの頭でDB接続する。
deferでcloseを忘れずに。
後は普通にテストを記述する。
テストが複数あってもテストデータは混ざらないし、テスト終了後にテスト中で作成したデータは削除される。

func TestLoginUser(t *testing.T) {
	db, _ := NewTestDB("TestLoginUser")
	defer func() {
		db, _ := db.DB()
		db.Close()
	}()

	db.Create(
		&usecase.User{
			Name:     "test1",
			Email:    "test1@example.com",
			Password: "test1",
		},
	)

	type args struct {
		email    string
		password string
	}
	tests := []struct {
		name    string
		args    args
		want    *usecase.User
		wantErr bool
	}{
		{
			name: "normal",
			args: args{
				email:    "test1@example.com",
				password: "test1",
			},
			want: &usecase.User{
				Name:     "test1",
				Email:    "test1@example.com",
				Password: "test1",
			},
			wantErr: false,
		},
		{
			name: "exists",
			args: args{
				email:    "test@example.com",
				password: "test",
			},
			want:    nil,
			wantErr: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert := assert.New(t)
			got, err := usecase.LoginUser(db, tt.args.email, tt.args.password)
			if (err != nil) != tt.wantErr {
				t.Errorf("LoginUser() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if tt.want == nil {
				assert.Nil(got)
				return
			}
			assert.Equal(got.Name, tt.want.Name)
			assert.Equal(got.Email, tt.want.Email)
			assert.Equal(got.Password, tt.want.Password)
		})
	}
}

Discussion