如何使用Mockgen为Go项目的Sqlx数据库实现Mock测试
sqlx没有自动生成接口的能力,手动做一层接口抽象就能适配Mockgen完成数据库Mock,具体步骤如下:
第一步:抽离数据库操作接口
你当前代码里handler直接依赖*sqlx.DB具体实现,本身就不支持Mock,先把所有业务用到的数据库操作统一抽象成接口。建议在internal/model下新建store.go,定义Store接口,把你internal/queries/queries.go里所有数据库操作方法的签名都列进去,同时写一个基于sqlx的真实实现结构体,封装*sqlx.DB实现这个接口。
示例代码:// internal/model/store.go package model import ( "context" "github.com/jmoiron/sqlx" ) // Store 定义所有业务需要的数据库操作 type Store interface { GetUser(ctx context.Context, id int64) (*User, error) GetAllUser(ctx context.Context) ([]*User, error) CreateUser(ctx context.Context, u *User) error GetUserAddress(ctx context.Context, userID int64) ([]*Address, error) CreateUserAddress(ctx context.Context, userID int64, addr *Address) error UpdateUser(ctx context.Context, u *User) error UpdateUserAddress(ctx context.Context, addrID int64, addr *Address) error DisableUser(ctx context.Context, id int64) error EnableUser(ctx context.Context, id int64) error DeleteUser(ctx context.Context, id int64) error DeleteUserAddress(ctx context.Context, addrID int64) error } // SqlxStore 是Store接口的真实数据库实现 type SqlxStore struct { db *sqlx.DB } // 实现Store接口的所有方法,把你原来queries.go里的查询逻辑直接挪过来就行 func (s *SqlxStore) GetUser(ctx context.Context, id int64) (*User, error) { var u User err := s.db.GetContext(ctx, &u, "SELECT * FROM users WHERE id = $1", id) return &u, err } // 其余方法按同样逻辑实现,把原来函数里的db.xxx调用换成s.db.xxx即可 func NewSqlxStore(db *sqlx.DB) Store { return &SqlxStore{db: db} }如果觉得手动写接口签名麻烦,用接口生成工具扫queries.go里的函数直接生成即可,不用逐个敲。
第二步:替换现有代码的依赖类型
把项目里所有直接依赖*sqlx.DB的地方,全换成model.Store接口类型:- 改
internal/api/app.go里的App结构体:
type App struct { Router *httprouter.Router DB model.Store // 原来的*sqlx.DB替换成接口 }- 改App初始化逻辑,原来的
a.DB = model.DBMigrate(db)替换成a.DB = model.NewSqlxStore(model.DBMigrate(db)) - 所有handler函数的入参,原来第一个参数是
*sqlx.DB的,全换成model.Store,业务逻辑不用做任何修改,因为接口方法和你之前写的查询函数签名完全一致。
- 改
第三步:用Mockgen生成Mock实现
先安装mockgen工具,执行命令:go install go.uber.org/mock/mockgen@latest然后在项目根目录执行生成命令,自动生成mock代码:
mockgen -source=internal/model/store.go -destination=internal/mocks/mock_store.go -package=mocks生成的mock代码里已经自带了期望调用设置、返回值自定义的能力,不用手写任何mock逻辑。
第四步:单元测试中使用Mock
写测试的时候直接初始化MockStore,预设对应方法的返回值,传给handler即可,不会触发真实数据库连接。示例:// internal/handler/users_test.go package handler import ( "GO_APP/internal/mocks" "GO_APP/internal/model" "net/http" "net/http/httptest" "testing" "github.com/julienschmidt/httprouter" "go.uber.org/mock/gomock" "github.com/stretchr/testify/assert" ) func TestGetUser(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() // 初始化mock数据库实例 mockStore := mocks.NewMockStore(ctrl) // 预设调用行为:传入任意context、用户id=1时,返回测试用的用户数据 testUser := &model.User{ID: 1, Name: "test", Email: "test@example.com"} mockStore.EXPECT().GetUser(gomock.Any(), int64(1)).Return(testUser, nil) // 构造测试请求 req := httptest.NewRequest(http.MethodGet, "/users/1", nil) w := httptest.NewRecorder() ps := httprouter.Params{{Key: "id", Value: "1"}} // 传入mockStore调用handler,无真实DB交互 GetUser(mockStore, w, req, ps) // 校验返回结果 assert.Equal(t, http.StatusOK, w.Code) }
额外说明:手动抽Store接口不是额外工作量,这种分层方式本身就会降低代码耦合度,后续要换数据库驱动、换ORM,只要实现Store接口就行,上层handler、api层的代码完全不用改,比直接依赖具体的
*sqlx.DB结构更易维护。
内容的提问来源于stack exchange,提问作者Pygirl

