概要
DBの単体テストを行う際に、実際のDBを準備するのではなく、
sql-mockを利用してモックを作成してDB操作のテストを実施する
ここではCreate, Get操作のみを対象とする
[!warning!]
Go歴半年(あそび)です。
情報に誤りがある可能性があります
go-sqlmock
install
go get github.com/DATA-DOG/go-sqlmock
- DB: PostgreSQL
- ORM: gorm
テスト対象コード
domian/user.go
各種インターフェースも定義していますが、ここでは割愛
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Password string `json:"password"`
}
repository/repository.go
type userRepository struct {
db *gorm.DB
}
func NewUserReposiotry(db *gorm.DB) domain.UserRepository {
return &userRepository{
db: db,
}
}
func (ur *userRepository) Create(ctx context.Context, user *domain.User) error {
return ur.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Create(user).Error; err != nil {
return fmt.Errorf("create error: %v", err)
}
return nil
})
}
func (ur *userRepository) GetUser(ctx context.Context, id int) (*domain.User, error) {
var user domain.User
if err := ur.db.WithContext(ctx).Where("id = ?", id).Take(&user).Error; err != nil {
return nil, err
}
return &user, nil
}
func (ur *userRepository) GetAllUser(ctx context.Context) ([]domain.User, error) {
var users []domain.User
if err := ur.db.WithContext(ctx).Find(&users).Error; err != nil {
return nil, err
}
return users, nil
}
func (ur *userRepository) Delete(ctx context.Context, id int) error {
return ur.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("id = ?", id).Delete(&domain.User{}).Error; err != nil {
return fmt.Errorf("delete error: %v", err)
}
return nil
})
}
テスト対象コード
Table Driven Testを利用して、可読性、再利用性を意識して実装する
repository/repository_test.go
mockの初期化を行うコードを共通化
func getDBMock(t *testing.T) (*gorm.DB, sqlmock.Sqlmock, func()) {
// SQLMockの初期化
db, mock, err := sqlmock.New()
require.NoError(t, err, "failed to create SQL mock")
// GORMの初期化
gdb, err := gorm.Open(postgres.New(postgres.Config{
Conn: db,
}), &gorm.Config{})
require.NoError(t, err, "failed to open gorm DB connection")
tearDown := func() {
db.Close()
}
return gdb, mock, tearDown
}
TestCreateUser
ポイントはmock.ExpectQuery
を利用する点
go-sqlmockのREADMEには、INSERT INTOのテストを行う際は
mock.ExpectExec
を利用するように記載されているが、PostgreSQLのCreateには戻り値があるため、mock.ExpectQuery
を利用する
func TestCreateUser(t *testing.T) {
tests := []struct {
title string
user *domain.User
query string
expectedError bool
}{
{
"create a user successfully",
&domain.User{
Name: "sample name",
Email: "test@test.co.jp",
Password: "secret",
},
`INSERT INTO "users" ("name","email","password") VALUES ($1,$2,$3)`,
false,
},
{
"create a user with error",
&domain.User{
Name: "sample name",
Email: "test@test.co.jp",
Password: "secret",
},
`INSERT INTO "users" ("name","email","password") VALUES ($1,$2,$3)`,
true,
},
}
for _, tt := range tests {
t.Run(tt.title, func(t *testing.T) {
// mock
db, mock, teardown := getDBMock(t)
defer teardown()
mock.MatchExpectationsInOrder(false)
mock.ExpectBegin()
if tt.expectedError {
mock.ExpectQuery(regexp.QuoteMeta(tt.query)).
WithArgs(tt.user.Name, tt.user.Email, tt.user.Password, tt.user.CreatedAt).
WillReturnError(fmt.Errorf("create user error"))
mock.ExpectRollback()
} else {
mock.ExpectQuery(regexp.QuoteMeta(tt.query)).
WithArgs(tt.user.Name, tt.user.Email, tt.user.Password).WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1)
mock.ExpectCommit()
}
// run
r := repository.NewUserReposiotry(db)
err := r.Create(context.TODO(), tt.user)
// assert
if tt.expectedError {
require.Error(t, err)
} else {
require.NoError(t, err)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("there were unfulfilled expectations: %s", err)
}
})
}
}
TestGetUser
func TestGetUser(t *testing.T) {
columns := []string{"id", "name", "email", "password"}
tests := []struct {
title string
id int
query string
mockRow []driver.Value
expected *domain.User
expectedError bool
}{
{
"get a user successfully",
1,
`SELECT * FROM "users" WHERE id = $1 LIMIT $2`,
[]driver.Value{1, "sample name", "test@test.co.jp", "secret", strToTime("2024-11-27 08:57:01")},
&domain.User{
ID: 1,
Name: "sample name",
Email: "test@test.co.jp",
Password: "secret",
},
false,
},
{
"get a user with error",
1,
`SELECT * FROM "users" WHERE id = $1 LIMIT $2`,
[]driver.Value{1, "sample name", "test@test.co.jp", "secret", strToTime("2024-11-27 08:57:01")},
&domain.User{
ID: 1,
Name: "sample name",
Email: "test@test.co.jp",
Password: "secret",
},
true,
},
}
for _, tt := range tests {
t.Run(tt.title, func(t *testing.T) {
// mock
db, mock, teardown := getDBMock(t)
defer teardown()
if tt.expectedError {
mock.ExpectQuery(regexp.QuoteMeta(tt.query)).
WithArgs(tt.id, 1).
WillReturnError(fmt.Errorf("get user error"))
} else {
rows := sqlmock.NewRows(columns).AddRow(tt.mockRow...)
mock.ExpectQuery(regexp.QuoteMeta(tt.query)).
WithArgs(tt.id, 1).
WillReturnRows(rows)
}
// run
r := repository.NewUserReposiotry(db)
actual, err := r.GetUser(context.TODO(), tt.id)
// assert
if tt.expectedError {
require.Error(t, err)
} else {
require.NoError(t, err)
require.Equal(t, tt.expected, actual)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("there were unfulfilled expectations: %s", err)
}
})
}
}
参考
GitHub - DATA-DOG/go-sqlmock: Sql mock driver for golang to test database interactions