0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

gorm(PostgreSQL)単体テスト (go-sqlmock)

Last updated at Posted at 2024-11-28

概要

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

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?