能彻底脱离真实数据库,但前提是模型层不硬编码*sql.DB、所有SQL路径都被显式Expect且事务流程手动控制;否则测试会panic或静默失败。

能彻底脱离真实数据库,但前提是模型层不硬编码 *sql.DB、所有 SQL 路径都被显式 Expect,且事务流程手动控制——否则测试会 panic 或静默失败。
模型层必须接收可替换的 DB 依赖
如果业务函数内部直接调用 sql.Open 或 struct 里 embed 了 *sql.DB,sqlmock 根本插不进去。必须把数据访问能力暴露为参数或接口:
- 推荐方式:定义
type Querier interface { QueryRowContext(); ExecContext(); QueryContext() },模型方法接收该接口 - 兼容方式:函数签名改为
func GetUser(db *sql.DB, id int) (*User, error),测试时传入sqlmock.New()返回的实例 - 避免方式:在 handler 或 service 里 new 一个
*sql.DB并直接用——这种代码没法测,得先重构
ExpectQuery 和 ExpectExec 必须严格匹配 SQL 字符串
sqlmock 默认不做空格归一化、不忽略换行、不兼容占位符风格差异,写错一个空格就匹配失败:
- MySQL 用户:业务代码用
?,Expect 就得写ExpectQuery("SELECT * FROM users WHERE id = ?"),不能多空格、不能写成=? - PostgreSQL 用户:业务用
$1,Expect 也得用$1;若想宽松匹配,初始化 mock 时加sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual) - 更鲁棒的做法:用
regexp.QuoteMeta("SELECT * FROM users") + ` WHERE id = \?`构造正则,但优先用字面量,减少不确定性
返回 rows 时列名和顺序必须与 Scan 完全一致
sqlx.StructScan 或原生 Scan 对字段名大小写、顺序极其敏感,错一点就跳过字段或 panic:
- 用
mock.NewRows([]string{"id", "name", "created_at"})显式声明列头,顺序必须和Scan(&id, &name, &created_at)一致 - 结构体 tag 是
db:"user_id",列名就得写"user_id",不能写"id" - 时间字段别传字符串,用
sqlmock.NewNullTime(time.Now(), true);否则Scan因类型不匹配失败 - 空结果集也要调
WillReturnRows(mock.NewRows([]string{"id"})),漏掉就会 panic
事务逻辑必须手动注册 Begin/Commit/Rollback
模型层若封装了 tx, _ := db.Begin(),sqlmock 不会自动感知——它只认你注册的期望:
- 先调
mock.ExpectBegin(),再用mockDB.Begin()获取 mock tx - 对
tx.QueryRowContext()的调用,必须单独用mock.ExpectQuery().WithinTransact()绑定到该 tx - 覆盖 rollback 场景:调
mock.ExpectRollback(),然后手动触发错误路径 - 末尾必须调
mock.ExpectationsWereMet(),否则未匹配的期望不会报错,测试看似通过实则失效
最常被忽略的是列名大小写和事务内查询绑定——这两处出问题时往往没有明显报错,而是字段为空或测试假通过。


















