GORMとgo-txdb(+Gin)で、DBを使ったテストを安全に実行する
この記事でわかること
go-txdbを使ってgormでDBの読み書きを行うテストを安全に実行できるようにします。
usecaseおよびHTTPハンドラでテストを実施します。
概要
DB読み書きを伴う自動テストを記述するとき、あるテストが別のテストで作成されたテストデータの影響を受けてテストが失敗してしまうことがあります。
go-txdbを使うと、テストケース(コネクション)ごとにテストデータを分離できるので、テストケース間でテストデータが汚染されることを気にする必要がなくなったり、テスト終了後にテストデータの削除を気にする必要がなくなります。
登場するライブラリ
テストの書き方について
コード
テストごとのDBコネクション作成とデータ投入
gormインスタンスを作成する前にtxdb.Registerを呼び出しておく。
テストごとにコネクションを作成してテストを実行する。
nameはテストデータを分離してほしい単位(今回はテスト関数ごと)でユニークにしておく。
sqlite.NewのDriverNameとしてnameを渡すとtxdb経由でDBコネクションが生成され、異なるnameとはデータが混在しなくなる。
txdb経由でコネクションを生成する例
func NewTestDB(name string) (*gorm.DB, error) {
// nameはパッケージ全体でユニークにする。重複するとエラーとなる。
// dsnはRDBMSごとのDSNを渡す(sqliteの場合はDBファイルへのパス)
txdb.Register(name, "sqlite3", dsn)
// コネクションを生成する際はDriverNameとしてnameを渡す
dialector := sqlite.New(sqlite.Config{
DriverName: name,
DSN: dsn,
})
// 生成したコネクションでgormインスタンスを作成
db, err := gorm.Open(dialector, &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
})
if err != nil {
return nil, err
}
// コネクションを作成したらテーブル作成したり初期データを投入する
initDB(db)
return db, nil
}
各テストの頭で上記関数を使ってコネクションを生成する。
その際忘れずにdeferでdbを閉じること。
後は普通にテストを記述する。
テストが複数あってもテストデータは混ざらないし、テスト終了後にテスト中で作成したデータは削除される。
テストごとにユニークなnameでコネクションを作成する都合上、TestMainでコネクションを作成したりテストデータを投入はできないと思われる。
usecaseレベルのテスト例
必要事項はコード中のコメントとして書く
func TestLoginUser(t *testing.T) {
// コネクションを作成して初期データが生成される
db, _ := NewTestDB("TestLoginUser")
defer func() {
// *** 忘れずにcloseする ***
db, _ := db.DB()
db.Close()
}()
// テストごとのデータ投入。
db.Create(
&usecase.User{
Name: "test1",
Email: "test1@example.com",
Password: "test1",
},
)
// あとは普通にテストを実装する。
// テスト結果の確認については、DBから直接値を取得してもよい。
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)
})
}
}
HTTPルータを経由したテスト例
内部的にHTTPルータを起動しハンドラを呼び出してテストすることができる。
Ginでのやり方はTestingのページに記載されている。
普通にルータを起動し、テスト用のhttptest.NewRecorder
をhttp.NewRequest
に渡すことでHTTPリクエストを呼び出し、結果を記録・検証できる。
func TestUserHandler_CreateUser(t *testing.T) {
// ユニークなname(ここでは関数名)を渡す
db, _ := testutil.NewTestDB("TestUserHandler_CreateUser")
defer func() {
// *** 忘れずにclose ***
db, _ := db.DB()
db.Close()
}()
// テストデータ作成
db.Create(
&usecase.User{
Name: "testname2",
Email: "test2@example.com",
Password: "testpass2",
},
)
// テスト用のコネクションを使ってルーターを起動する
h := &handler.UserHandler{
DB: db,
}
router := handler.SetupRouter()
router = h.CreateUser(router)
tests := []struct {
name string
req handler.CreateUserRequest
status int
want usecase.User
}{
{
name: "create success",
req: handler.CreateUserRequest{
Name: "testname1",
Email: "test1@example.com",
Password: "testpass1",
},
status: http.StatusOK,
want: usecase.User{
ID: 1,
Name: "testname1",
Email: "test1@example.com",
Password: "testpass1",
},
},
{
name: "exists",
req: handler.CreateUserRequest{
Name: "testname2",
Email: "test2@example.com",
Password: "testpass2",
},
status: http.StatusOK,
want: usecase.User{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert := assert.New(t)
// テストケースごとにRecorderを作成して呼び出し
w := httptest.NewRecorder()
reqJson, _ := json.Marshal(tt.req)
req, _ := http.NewRequest("POST", "/user/add", bytes.NewBuffer(reqJson))
router.ServeHTTP(w, req)
// httptest.Recorderを使って呼び出し結果を検証する
assert.Equal(tt.status, w.Code)
if tt.status == http.StatusOK {
var got usecase.User
json.Unmarshal(w.Body.Bytes(), &got)
assert.Equal(tt.want.Name, got.Name)
assert.Equal(tt.want.Email, got.Email)
assert.Equal(tt.want.Password, got.Password)
// DB中のデータを直接検証できる
assert.NoError(db.Where("email = ?", tt.req.Email).First(&got).Error)
assert.Equal(tt.req.Name, got.Name)
assert.Equal(tt.req.Email, got.Email)
assert.Equal(tt.req.Password, got.Password)
}
})
}
}
Discussion