单元测试的理想状态是只测试被测代码,不依赖外部系统。但实际项目中,代码往往依赖数据库、缓存、第三方 API 等。Mock 测试就是用模拟对象替代真实依赖,让测试更快速、更可控。
假设你有一个用户服务,依赖数据库:
type UserService struct {
db *sql.DB
}
func (s *UserService) GetUser(id int) (*User, error) {
var user User
err := s.db.QueryRow("SELECT * FROM users WHERE id = ?", id).Scan(&user.ID, &user.Name)
if err != nil {
return nil, err
}
return &user, nil
}
直接测试这个方法需要真实的数据库连接,这会带来问题:
Mock 可以解决这些问题。
Mock 的前提是依赖抽象而非具体实现。首先定义接口:
// 数据访问接口
type UserRepository interface {
FindByID(id int) (*User, error)
Create(user *User) error
Update(user *User) error
Delete(id int) error
}
// 用户服务
type UserService struct {
repo UserRepository
}
func NewUserService(repo UserRepository) *UserService {
return &UserService{repo: repo}
}
func (s *UserService) GetUser(id int) (*User, error) {
return s.repo.FindByID(id)
}
这样,UserService 依赖的是接口,而不是具体的数据库实现。
最简单的 Mock 是手写一个实现:
type MockUserRepository struct {
users map[int]*User
}
func NewMockUserRepository() *MockUserRepository {
return &MockUserRepository{
users: make(map[int]*User),
}
}
func (m *MockUserRepository) FindByID(id int) (*User, error) {
user, ok := m.users[id]
if !ok {
return nil, errors.New("user not found")
}
return user, nil
}
func (m *MockUserRepository) Create(user *User) error {
m.users[user.ID] = user
return nil
}
func (m *MockUserRepository) Update(user *User) error {
m.users[user.ID] = user
return nil
}
func (m *MockUserRepository) Delete(id int) error {
delete(m.users, id)
return nil
}
使用 Mock 进行测试:
func TestUserService_GetUser(t *testing.T) {
// 创建 Mock
mockRepo := NewMockUserRepository()
mockRepo.Create(&User{ID: 1, Name: "Alice"})
// 创建服务
service := NewUserService(mockRepo)
// 测试
user, err := service.GetUser(1)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if user.Name != "Alice" {
t.Errorf("Expected name Alice, got %s", user.Name)
}
// 测试不存在的用户
_, err = service.GetUser(999)
if err == nil {
t.Error("Expected error for non-existent user")
}
}
手写 Mock 对于简单场景够用,但复杂场景下维护成本高。testify 提供了更强大的 Mock 功能:
import (
"testing"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/assert"
)
// Mock 实现
type MockUserRepository struct {
mock.Mock
}
func (m *MockUserRepository) FindByID(id int) (*User, error) {
args := m.Called(id)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*User), args.Error(1)
}
func (m *MockUserRepository) Create(user *User) error {
args := m.Called(user)
return args.Error(0)
}
func (m *MockUserRepository) Update(user *User) error {
args := m.Called(user)
return args.Error(0)
}
func (m *MockUserRepository) Delete(id int) error {
args := m.Called(id)
return args.Error(0)
}
使用 testify/mock 的测试:
func TestUserService_GetUser(t *testing.T) {
mockRepo := new(MockUserRepository)
service := NewUserService(mockRepo)
// 设置期望
mockRepo.On("FindByID", 1).Return(&User{ID: 1, Name: "Alice"}, nil)
mockRepo.On("FindByID", 999).Return(nil, errors.New("not found"))
// 测试存在的用户
user, err := service.GetUser(1)
assert.NoError(t, err)
assert.Equal(t, "Alice", user.Name)
// 测试不存在的用户
_, err = service.GetUser(999)
assert.Error(t, err)
// 验证所有期望都被调用
mockRepo.AssertExpectations(t)
}
testify/mock 的优势:
假设你的服务需要调用外部 API:
type PaymentClient interface {
Charge(userID int, amount float64) (*PaymentResult, error)
}
type OrderService struct {
payment PaymentClient
}
func (s *OrderService) CreateOrder(userID int, amount float64) error {
result, err := s.payment.Charge(userID, amount)
if err != nil {
return err
}
// 处理订单逻辑...
return nil
}
Mock 支付服务:
type MockPaymentClient struct {
mock.Mock
}
func (m *MockPaymentClient) Charge(userID int, amount float64) (*PaymentResult, error) {
args := m.Called(userID, amount)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*PaymentResult), args.Error(1)
}
func TestOrderService_CreateOrder(t *testing.T) {
mockPayment := new(MockPaymentClient)
service := &OrderService{payment: mockPayment}
t.Run("Successful payment", func(t *testing.T) {
mockPayment.On("Charge", 1, 100.0).Return(&PaymentResult{Success: true}, nil)
err := service.CreateOrder(1, 100.0)
assert.NoError(t, err)
mockPayment.AssertExpectations(t)
})
t.Run("Failed payment", func(t *testing.T) {
mockPayment.On("Charge", 1, 100.0).Return(nil, errors.New("insufficient funds"))
err := service.CreateOrder(1, 100.0)
assert.Error(t, err)
mockPayment.AssertExpectations(t)
})
}
结合 Gin 的依赖注入模式:
// Handler 结构体
type UserHandler struct {
service *UserService
}
func NewUserHandler(service *UserService) *UserHandler {
return &UserHandler{service: service}
}
func (h *UserHandler) GetUser(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
user, err := h.service.GetUser(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
return
}
c.JSON(http.StatusOK, user)
}
// 路由设置
func SetupRouter(handler *UserHandler) *gin.Engine {
router := gin.New()
router.GET("/users/:id", handler.GetUser)
return router
}
测试 Handler:
func TestUserHandler_GetUser(t *testing.T) {
gin.SetMode(gin.TestMode)
// 创建 Mock
mockRepo := new(MockUserRepository)
service := NewUserService(mockRepo)
handler := NewUserHandler(service)
router := SetupRouter(handler)
t.Run("User exists", func(t *testing.T) {
mockRepo.On("FindByID", 1).Return(&User{ID: 1, Name: "Alice"}, nil)
req := httptest.NewRequest("GET", "/users/1", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
mockRepo.AssertExpectations(t)
})
t.Run("User not found", func(t *testing.T) {
mockRepo.On("FindByID", 999).Return(nil, errors.New("not found"))
req := httptest.NewRequest("GET", "/users/999", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusNotFound, w.Code)
mockRepo.AssertExpectations(t)
})
}
gomock 是官方提供的 Mock 工具,配合 mockgen 可以自动生成 Mock 代码:
安装:
go install github.com/golang/mock/mockgen@latest
定义接口:
//go:generate mockgen -source=repository.go -destination=mock_repository.go -package=mocks
type UserRepository interface {
FindByID(id int) (*User, error)
Create(user *User) error
}
生成 Mock:
go generate ./...
使用生成的 Mock:
import (
"testing"
"github.com/golang/mock/gomock"
"your-project/mocks"
)
func TestUserService_GetUser(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockRepo := mocks.NewMockUserRepository(ctrl)
service := NewUserService(mockRepo)
// 设置期望
mockRepo.EXPECT().
FindByID(1).
Return(&User{ID: 1, Name: "Alice"}, nil)
user, err := service.GetUser(1)
assert.NoError(t, err)
assert.Equal(t, "Alice", user.Name)
}
gomock 的优势:
不要 Mock 你无法控制的类型,比如标准库或第三方库的类型。应该封装一层接口。
不是所有测试都需要 Mock。集成测试应该使用真实依赖,只有单元测试才需要 Mock。
Mock 的行为应该简单明了,不要在 Mock 中实现复杂逻辑。
使用 AssertExpectations 或类似机制确保 Mock 方法被正确调用。
通过构造函数注入依赖,而不是在函数内部创建依赖。
Mock 测试是单元测试的重要技术:
Mock 的关键在于设计良好的接口。如果代码耦合度高,Mock 会变得困难。所以 Mock 测试也在倒逼我们写出更好的代码结构。