sqlmock.New() 默认启用严格模式,任何未通过 Expect 预定义的 SQL 语句都会触发 panic。开发者必须显式声明所有查询、列名、事务操作及参数匹配规则,并且所有 Expect 调用必须在对应 SQL 执行之前完成注册。

在 Go 单元测试中引入 SQLMock 本意是隔离数据库依赖,然而许多开发者初次使用时就会遇到“执行 SQL 立即 panic”的困境。以下列举几个典型场景,几乎覆盖了新手常犯的错误,我们逐一剖析。
为什么调用 sqlmock.New() 后执行 SQL 会立即 panic?
问题根源在于 sqlmock.New() 默认开启了严格模式:任何未被 Expect 预定义的 SQL 语句都会立即引发 panic,例如 panic: there is no expectation for "SELECT * FROM users"。这并非缺陷,而是设计上强制要求开发者显式声明所有数据库交互行为。
- 不要指望它能自动豁免健康检查语句——
db.Ping()同样会直接 panic,必须手动编写mock.ExpectQuery("SELECT 1").WillReturnRows(...)来匹配 - 空格、换行符和大小写必须完全一致,
"SELECT id FROM users"与"SELECT\nid\nFROM users"会被视为两条不同的 SQL 语句 - 对于不确定的参数(例如时间戳、UUID),不要硬编码具体值,应使用
sqlmock.AnyArg()替代,否则参数匹配会失败
Scan 或 StructScan 报错“expected 3 destination arguments, not 2”如何解决?
根本原因在于 mock 返回的 rows 列定义与 Scan 目标不一致。SQLMock 不会进行类型推断,仅依据你声明的列名和顺序进行严格校验。
- 必须使用
sqlmock.NewRows([]string{"id", "name", "created_at"})显式声明列名,其顺序和数量必须与rows.Scan(&id, &name, &created_at)完全一致 - 若使用
sqlx.StructScan,结构体字段的db标签(例如db:"user_id")必须与 rows 列名完全一致;建议统一采用小写加下划线的命名风格 - 时间字段应传入
time.Now()或sqlmock.NewNullTime(),切勿传递字符串,否则类型不匹配会直接报错
事务测试中 tx.Commit() 未报错但测试却失败了?
SQLMock 对事务操作默认保持静默,tx.Commit() 是否成功完全取决于你是否提前调用了 ExpectCommit()。如果没有 Expect,则意味着该语句不应发生,进而导致 panic。
- 每个
Begin()调用都必须有对应的ExpectBegin(),即使业务逻辑中嵌套了多个Begin()也是如此 Commit和Rollback必须二选一进行显式 Expect,不可遗漏db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable})中的隔离级别设置会被忽略,SQLMock 仅关注流程,不校验事务选项
ExpectationsWereMet() 总是返回 false,但明明已经写了 Expect?
大概率是 Expect 注册时机不对——它必须在 SQL 实际执行之前注册,而不是在 defer 中或函数末尾补充。
- 常见错误:先执行
defer mock.ExpectationsWereMet(),再编写mock.ExpectQuery(...),导致 Expect 尚未注册时 SQL 就已经被执行 - 同一 SQL 语句被执行多次(例如循环查询用户),则需要多次调用
mock.ExpectQuery().WillReturnRows(),不能只写一次 - 不要在不同测试函数之间复用同一个 mock 实例,每个测试应独立初始化
sqlmock.New()
最容易忽略的是列名的大小写和空格——它们不像真实数据库那样宽容,哪怕多一个空格或下划线位置有误,Expect 就永远无法命中。
