不是所有测试都需要真实数据库。Mock 数据库可以加快测试速度,隔离外部依赖。这一章介绍几种 Mock 数据库的方法。
适合 Mock 的场景:
不适合 Mock 的场景:
最常见的方式是把数据库操作抽象成接口:
type QueryExecutor interface {
Find(dest interface{}, conds ...interface{}) *gorm.DB
First(dest interface{}, conds ...interface{}) *gorm.DB
Create(value interface{}) *gorm.DB
Update(column string, value interface{}) *gorm.DB
Delete(value interface{}, conds ...interface{}) *gorm.DB
}
用 testify/mock 实现:
type MockDB struct {
mock.Mock
}
func (m *MockDB) Find(dest interface{}, conds ...interface{}) *gorm.DB {
args := m.Called(dest, conds)
return args.Get(0).(*gorm.DB)
}
func (m *MockDB) First(dest interface{}, conds ...interface{}) *gorm.DB {
args := m.Called(dest, conds)
return args.Get(0).(*gorm.DB)
}
func (m *MockDB) Create(value interface{}) *gorm.DB {
args := m.Called(value)
return args.Get(0).(*gorm.DB)
}
测试用例:
func TestGetUser(t *testing.T) {
mockDB := new(MockDB)
user := &User{ID: 1, Name: "张三"}
mockDB.On("First", mock.Anything, []interface{}{uint(1)}).
Return(&gorm.DB{Error: nil}).
Run(func(args mock.Arguments) {
dest := args.Get(0).(*User)
*dest = *user
})
repo := &UserRepository{db: mockDB}
result, err := repo.GetByID(1)
assert.NoError(t, err)
assert.Equal(t, user, result)
}
直接 Mock 底层的 SQL 连接:
import "github.com/DATA-DOG/go-sqlmock"
func setupMockDB(t *testing.T) (*gorm.DB, sqlmock.Sqlmock) {
sqlDB, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("failed to create mock: %v", err)
}
gdb, err := gorm.Open(mysql.New(mysql.Config{
Conn: sqlDB,
SkipInitializeWithVersion: true,
}), &gorm.Config{})
if err != nil {
t.Fatalf("failed to open gorm: %v", err)
}
return gdb, mock
}
func TestFindUsers(t *testing.T) {
db, mock := setupMockDB(t)
// 设置期望
rows := sqlmock.NewRows([]string{"id", "name", "email"}).
AddRow(1, "张三", "zhangsan@example.com").
AddRow(2, "李四", "lisi@example.com")
mock.ExpectQuery("SELECT \\* FROM `users`").
WillReturnRows(rows)
// 执行查询
var users []User
db.Find(&users)
assert.Len(t, users, 2)
assert.Equal(t, "张三", users[0].Name)
// 验证期望是否满足
assert.NoError(t, mock.ExpectationsWereMet())
}
sqlmock 可以精确控制返回的数据和错误:
// 模拟错误
mock.ExpectQuery("SELECT \\* FROM `users`").
WillReturnError(errors.New("connection lost"))
// 模拟插入
mock.ExpectBegin()
mock.ExpectExec("INSERT INTO `users`").
WithArgs("张三", "zhangsan@example.com").
WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit()
// 模拟事务
mock.ExpectBegin()
mock.ExpectExec("UPDATE `users`").
WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectRollback()
GORM 有个 DryRun 模式,只生成 SQL 不执行:
func TestGeneratedSQL(t *testing.T) {
db, _ := gorm.Open(mysql.Open("root:@/test"), &gorm.Config{
DryRun: true,
})
stmt := db.Where("name = ?", "张三").First(&User{}).Statement
sql := stmt.SQL.String()
assert.Contains(t, sql, "SELECT * FROM `users` WHERE name = ?")
}
这个模式适合验证生成的 SQL 是否正确。
更推荐的做法是用 Repository 模式:
type UserRepository interface {
GetByID(ctx context.Context, id uint) (*User, error)
GetByEmail(ctx context.Context, email string) (*User, error)
Create(ctx context.Context, user *User) error
Update(ctx context.Context, user *User) error
Delete(ctx context.Context, id uint) error
}
// 真实实现
type gormUserRepository struct {
db *gorm.DB
}
func (r *gormUserRepository) GetByID(ctx context.Context, id uint) (*User, error) {
var user User
err := r.db.WithContext(ctx).First(&user, id).Error
return &user, err
}
// Mock 实现
type mockUserRepository struct {
users map[uint]*User
}
func (r *mockUserRepository) GetByID(ctx context.Context, id uint) (*User, error) {
user, ok := r.users[id]
if !ok {
return nil, gorm.ErrRecordNotFound
}
return user, nil
}
func (r *mockUserRepository) Create(ctx context.Context, user *User) error {
user.ID = uint(len(r.users) + 1)
r.users[user.ID] = user
return nil
}
业务代码只依赖接口:
type UserService struct {
userRepo UserRepository
}
func (s *UserService) GetUser(ctx context.Context, id uint) (*User, error) {
return s.userRepo.GetByID(ctx, id)
}
测试:
func TestUserService_GetUser(t *testing.T) {
mockRepo := &mockUserRepository{
users: map[uint]*User{
1: {ID: 1, Name: "张三"},
},
}
service := &UserService{userRepo: mockRepo}
user, err := service.GetUser(context.Background(), 1)
assert.NoError(t, err)
assert.Equal(t, "张三", user.Name)
_, err = service.GetUser(context.Background(), 999)
assert.Equal(t, gorm.ErrRecordNotFound, err)
}
不要过度 Mock
Mock 太多会让测试变得脆弱,代码重构时测试也要改。只 Mock 必要的依赖。
Mock 行为要真实
Mock 的行为要尽量接近真实数据库:
// 不好:Mock 返回了真实数据库不会返回的数据
mockRepo.On("GetByID", 1).Return(&User{ID: 1, Name: ""}) // 名字不应该为空
// 好:Mock 返回合理的数据
mockRepo.On("GetByID", 1).Return(&User{ID: 1, Name: "测试用户"})
测试边界情况
Mock 可以轻松模拟各种异常:
// 数据库连接错误
mockRepo.On("GetByID", 1).Return(nil, errors.New("connection refused"))
// 超时
mockRepo.On("GetByID", 1).Return(nil, context.DeadlineExceeded)
// 重复键
mockRepo.On("Create", user).Return(errors.New("Duplicate entry"))
Mock 数据库的方法:
选择哪种方式取决于测试目标。业务逻辑测试用接口 Mock,SQL 行为测试用 sqlmock 或集成测试。