Gin Mock测试
Mock 测试通过模拟外部依赖,实现单元测试的隔离和可控性。
Mock 基础概念
为什么需要 Mock
Go
单元测试原则:
- 测试单个单元,不依赖外部系统
- 数据库、网络服务、文件系统等应 Mock
- 测试结果可重复、可预测
- 测试执行快速、无副作用
接口 Mock
定义接口
Go
// 定义数据访问接口
type UserRepository interface {
FindByID(id string) (*User, error)
FindAll() ([]*User, error)
Create(user *User) error
Update(user *User) error
Delete(id string) error
}
// Handler 依赖接口而非具体实现
type UserHandler struct {
repo UserRepository
}
func (h *UserHandler) GetUser(c *gin.Context) {
id := c.Param("id")
user, err := h.repo.FindByID(id)
if err != nil {
c.JSON(404, gin.H{"error": "用户不存在"})
return
}
c.JSON(200, user)
}
手动 Mock 实现
Go
type MockUserRepository struct {
users map[string]*User
}
func NewMockUserRepository() *MockUserRepository {
return &MockUserRepository{
users: map[string]*User{
"1": &User{ID: "1", Name: "Alice", Email: "alice@example.com"},
"2": &User{ID: "2", Name: "Bob", Email: "bob@example.com"},
},
}
}
func (m *MockUserRepository) FindByID(id string) (*User, error) {
if user, exists := m.users[id]; exists {
return user, nil
}
return nil, errors.New("用户不存在")
}
func (m *MockUserRepository) Create(user *User) error {
m.users[user.ID] = user
return nil
}
func (m *MockUserRepository) Delete(id string) error {
if _, exists := m.users[id]; exists {
delete(m.users, id)
return nil
}
return errors.New("用户不存在")
}
使用 Mock 测试
Bash
func TestGetUserHandler_WithMock(t *testing.T) {
mockRepo := NewMockUserRepository()
handler := &UserHandler{repo: mockRepo}
r := gin.New()
r.GET("/users/:id", handler.GetUser)
// 测试正常情况
req := httptest.NewRequest("GET", "/users/1", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
var user User
json.Unmarshal(w.Body.Bytes(), &user)
assert.Equal(t, "Alice", user.Name)
// 测试不存在情况
req = httptest.NewRequest("GET", "/users/999", nil)
w = httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, 404, w.Code)
}
gomock 自动生成
安装工具
Go
go install github.com/golang/mock/mockgen@latest
定义接口并生成 Mock
Bash
// interfaces.go
package repository
type UserRepository interface {
FindByID(id string) (*User, error)
Create(user *User) error
}
//go:generate mockgen -source=interfaces.go -destination=mock_repository.go -package=repository
Go
# 生成 Mock 文件
go generate ./...
使用 gomock 测试
Go
func TestGetUserHandler_Gomock(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockRepo := NewMockUserRepository(ctrl)
// 设置预期行为
mockRepo.EXPECT().
FindByID("1").
Return(&User{ID: "1", Name: "Alice"}, nil)
handler := &UserHandler{repo: mockRepo}
r := gin.New()
r.GET("/users/:id", handler.GetUser)
req := httptest.NewRequest("GET", "/users/1", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
}
func TestGetUserHandler_Error(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockRepo := NewMockUserRepository(ctrl)
// 设置错误预期
mockRepo.EXPECT().
FindByID("999").
Return(nil, errors.New("用户不存在"))
handler := &UserHandler{repo: mockRepo}
r := gin.New()
r.GET("/users/:id", handler.GetUser)
req := httptest.NewRequest("GET", "/users/999", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, 404, w.Code)
}
gomock 预期设置
Go
// 多次调用
mockRepo.EXPECT().
FindByID(gomock.Any()).
Return(&User{ID: "1"}, nil).
Times(3)
// 任意顺序
mockRepo.EXPECT().
FindByID("1").
Return(&User{ID: "1"}, nil).
AnyTimes()
// 参数匹配
mockRepo.EXPECT().
Create(gomock.Any()). // 任意参数
Return(nil)
// 自定义匹配器
mockRepo.EXPECT().
Create(&User{Name: "Alice"}).
Return(nil)
// 返回多次不同结果
mockRepo.EXPECT().
FindByID("1").
Return(&User{ID: "1"}, nil).
Return(nil, errors.New("并发冲突"))
数据库 Mock
GORM Mock
Go
type MockDB struct {
data map[string]interface{}
}
func (m *MockDB) Find(dest interface{}, conds ...interface{}) error {
// 模拟查询
return nil
}
func (m *MockDB) Create(value interface{}) error {
// 模拟创建
return nil
}
// 使用 sqlmock
import "github.com/DATA-DOG/go-sqlmock"
func TestDatabaseMock(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatal(err)
}
defer db.Close()
// 设置查询预期
rows := sqlmock.NewRows([]string{"id", "name"}).
AddRow(1, "Alice").
AddRow(2, "Bob")
mock.ExpectQuery("SELECT (.+) FROM users").
WillReturnRows(rows)
// 执行测试
// ...
}
HTTP 服务 Mock
Mock 第三方 API
Go
type APIClient interface {
GetUser(id string) (*User, error)
}
type MockAPIClient struct {
response *User
err error
}
func (m *MockAPIClient) GetUser(id string) (*User, error) {
return m.response, m.err
}
func TestExternalAPIMock(t *testing.T) {
mockClient := &MockAPIClient{
response: &User{ID: "1", Name: "External User"},
}
handler := ExternalAPIHandler{client: mockClient}
r := gin.New()
r.GET("/external/:id", handler.GetUser)
req := httptest.NewRequest("GET", "/external/1", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
}
httptest Mock Server
Go
func TestMockServer(t *testing.T) {
// 创建 Mock 服务器
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
w.Write([]byte(`{"id":"1","name":"Mock User"}`))
}))
defer mockServer.Close()
// 使用 Mock 服务器地址
client := NewAPIClient(mockServer.URL)
user, err := client.GetUser("1")
assert.NoError(t, err)
assert.Equal(t, "Mock User", user.Name)
}
缓存 Mock
Go
type CacheService interface {
Get(key string) (string, error)
Set(key string, value string, ttl time.Duration) error
}
type MockCache struct {
data map[string]string
}
func NewMockCache() *MockCache {
return &MockCache{data: make(map[string]string)}
}
func (m *MockCache) Get(key string) (string, error) {
if val, exists := m.data[key]; exists {
return val, nil
}
return "", errors.New("不存在")
}
func (m *MockCache) Set(key string, value string, ttl time.Duration) error {
m.data[key] = value
return nil
}
func TestWithCacheMock(t *testing.T) {
mockCache := NewMockCache()
mockCache.Set("user:1", `{"id":"1","name":"Cached"}`)
handler := CachedHandler{cache: mockCache}
r := gin.New()
r.GET("/cached/:id", handler.Get)
req := httptest.NewRequest("GET", "/cached/1", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), "Cached")
}
依赖注入模式
text
// 应用结构
type Application struct {
UserRepo UserRepository
Cache CacheService
APIClient APIClient
}
func NewApplicationForTest() *Application {
return &Application{
UserRepo: NewMockUserRepository(),
Cache: NewMockCache(),
APIClient: &MockAPIClient{},
}
}
func TestApplication(t *testing.T) {
app := NewApplicationForTest()
r := SetupRouter(app)
req := httptest.NewRequest("GET", "/users/1", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
}
Mock 最佳实践
| 实践 | 说明 |
|---|---|
| 接口定义 | 依赖接口而非具体实现 |
| 控制器管理 | gomock.NewController(t) |
| 预期验证 | ctrl.Finish() 检查预期是否满足 |
| 参数匹配 | gomock.Any() 匹配任意参数 |
| 次数控制 | Times(n)、AnyTimes() |
注意:Mock 应简化,避免 Mock 内部实现复杂逻辑。
要点总结
- 接口定义:依赖接口而非具体实现
- 手动 Mock:简单场景手动实现
- gomock:自动生成 Mock,支持预期验证
- 数据库 Mock:sqlmock 或接口 Mock
- HTTP Mock:httptest.NewServer 模拟外部服务
- 依赖注入:测试时注入 Mock 实现
📝 发现内容有误?点击此处直接编辑