直接用sqlmock拦截sql.DB调用最稳,因GORM核心类型非interface、gomock无法可靠模拟其链式调用与事务行为;正确做法是保留真实GORM流程,仅替换底层sql.DB为sqlmock实例,精确断言SQL执行细节。

直接用 sqlmock 拦截 *sql.DB 调用,而不是试图 mock 整个 GORM 实例或手写 fake DB —— 这是最稳、最贴近真实行为的路径。
为什么不用 gomock 生成 GORM 接口 mock?
GORM 的核心类型(如 *gorm.DB)不是 interface,它内部状态复杂、链式调用多、方法返回自身,gomock 无法可靠模拟其行为。强行抽象出 interface 并 mock,会导致测试与实际执行逻辑脱节:比如 Preload、Joins、事务嵌套等特性在 mock 中完全不可控,断言通过但线上仍出错。
正确做法是保留真实 GORM 初始化流程,只替换底层 *sql.DB 为 sqlmock 实例:
-
sqlmock拦截的是db.Query、db.Exec等原生调用,GORM 所有操作最终都落到这层 - 你能精确断言「是否执行了某条 SQL」「参数是否匹配」「返回行数/错误是否符合预期」
- 无需修改业务代码结构,不引入额外 interface 抽象,零侵入
sqlmock 基础用法:注册期望 + 验证执行
关键不是“让查询返回数据”,而是“确保 DAO 层按预期构造并发出 SQL”。例如测试一个根据 ID 查用户的函数:
func (d *UserDAO) GetByID(id string) (*User, error) {
var u User
err := d.db.Where("id = ?", id).First(&u).Error
return &u, err
}
对应测试应聚焦 SQL 构造逻辑:
- 调用
mock.ExpectQuery(`SELECT.*FROM users WHERE id = \?`),注意用正则匹配避免字面量硬编码 - 用
.WithArgs(id)断言参数传递正确 - 用
.WillReturnRows(mock.NewRows(...))提供模拟结果 - 最后必须调用
mock.AssertExpectations(t),否则未触发的期望会静默忽略
常见翻车点:事务、扫描、空结果处理
DAO 层常包含事务控制和结构体扫描逻辑,这些地方最容易在测试中漏掉覆盖:
- 事务内执行查询时,要确保 mock 是绑定到事务获取的
*sql.Tx,而非原始*sql.DB;sqlmock默认支持,但需用db.Begin()获取 tx 后再传给 DAO -
Scan或Rows.Scan失败时(比如列数不匹配),GORM 可能 panic;测试中要用.WillReturnError(fmt.Errorf("scan failed"))触发该分支 - 查不到数据时,GORM 返回
gorm.ErrRecordNotFound,但sqlmock默认返回sql.ErrNoRows;需显式.WillReturnError(gorm.ErrRecordNotFound)才能命中业务中的错误判断 - 别在 test 文件里调
defer db.Close()——sqlmock的db是假的,Close 会 panic
DAO 层测试和 Service 层测试怎么配合?
DAO 层用 sqlmock 验证 SQL 正确性,Service 层用 gomock 隔离 DAO 依赖,两者分工明确:
- DAO 测试:验证「输入参数 → 生成什么 SQL → 是否按预期执行」
- Service 测试:注入
gomock生成的UserRepomock,只关心「是否调用了repo.GetByID("123")」「是否对返回值做了正确处理」 - repo 接口必须定义在 service 包外(如
repo.UserRepo),否则mockgen无法生成实现 - 不要在 service 测试里重复验证 SQL —— 那是 DAO 层的事;也不要让 DAO 测试去校验业务规则(比如“VIP 用户返回折扣字段”)—— 那是 service 层的职责
真正难的不是写 mock,而是决定哪些 SQL 必须被断言:WHERE 条件、JOIN 表、ORDER BY 字段、是否用了 LIMIT —— 这些一旦写错,线上就慢或错,但单元测试里若只 mock 返回数据而忽略 SQL 本身,等于没测。


















