gorm单元测试不应直连真实数据库,因存在环境依赖、状态残留和性能问题;推荐用内存sqlite或sqlmock模拟,需复用db实例并正确清理数据。

为什么 GORM 的单元测试不能直接连真实数据库
因为真实数据库会引入环境依赖、状态残留和执行速度问题。比如 TestCreateUser 成功后,TestDeleteUser 可能因数据未清理而失败;CI 环境还可能没有数据库权限或网络不通。GORM 官方不推荐在单元测试中直连 DB,而是建议用 gorm.io/gorm/migrator 搭配内存 SQLite 或更轻量的 Mock 方案。
用 gomock + gormgen 实现分页查询的精准 Mock
直接 Mock db.Scopes(paginate.Paginate(...)) 很容易出错——因为 Paginate 是一个函数,不是接口方法,无法被 gomock 拦截。正确做法是:把分页逻辑抽成独立函数,接收 *gorm.DB 作为参数,并返回带 count 和 rows 的结构体;然后在测试中 Mock 这个函数的调用行为。
常见错误现象:mock.ExpectQuery("SELECT.*").WillReturnRows(...) 对分页 SQL 失效,因为 GORM v2 默认用 SELECT COUNT(*) + SELECT ... LIMIT OFFSET 两条语句,且语句含动态表名/别名,正则难匹配。
- 使用
gormmock.NewMockDB()(第三方库)仅适合简单 CRUD,对Scopes、Joins、Preload支持弱 - 推荐方案:用
sqlmock拦截底层*sql.DB,但需开启 GORM 的PrepareStmt: true,否则预编译语句无法被 sqlmock 捕获 - 分页 Mock 必须同时模拟两条 SQL:COUNT 查询返回 1 行 1 列的总数,主查询返回预期数据行
paginate.Paginate 在测试中必须传入非 nil 的 *gorm.DB
paginate.Paginate 是社区常用分页插件(如 github.com/lestrrat-go/paginate 或自定义实现),它内部会调用 db.Session(...).Count(&total) 和 db.Offset().Limit().Find()。如果测试中传入 nil 的 *gorm.DB,会 panic:panic: runtime error: invalid memory address or nil pointer dereference。
实操建议:
- 永远用
gorm.Open(sqlite.Open(":memory:"), &gorm.Config{...})创建一个干净的内存 DB 做基础 fixture - 在测试前用
AutoMigrate建表,避免ERROR: no such table - 若坚持纯 Mock,确保所有被
Paginate调用的链式方法(Session、Count、Offset、Limit、Find)都返回可链式调用的 mock 对象
性能陷阱:每次测试都新建 gorm.DB 实例会导致 PrepareStmt 失效
GORM 开启 PrepareStmt: true 后,会缓存预编译语句提升性能。但如果每个 test case 都调用 gorm.Open 新建 DB 实例,缓存就失效了,反而增加开销,且与生产行为不一致。
正确做法是复用 DB 实例:
- 在
func TestMain(m *testing.M)中初始化一次内存 DB,并在所有测试前自动 migrate - 每个 test case 用
db.Session(&gorm.Session{AllowGlobalUpdate: true}).Unscoped().Exec("DELETE FROM users")清空表,而不是重建 DB - 注意:SQLite 内存数据库的
:memory:每次 Open 都是全新实例,所以必须用file::memory:?cache=shared才能跨 session 共享
容易被忽略的是:GORM 的 Session 不影响底层 *sql.DB 连接池,但会影响预编译语句缓存键。同一个 *gorm.DB 实例下,不同 Session 的 PrepareStmt 是隔离的。











