直接用sqlmock.new()创建mock db并注入模型层可脱离真实数据库,前提是模型层不硬编码sql.db、接收querier接口或sql.db参数、显式expect所有sql路径,且严格匹配sql字符串、列名顺序、事务流程,并末尾调用mock.expectationsweremet()。

直接用 sqlmock.New() 创建 mock DB 并注入模型层,就能彻底脱离真实数据库——前提是模型层不硬编码 *sql.DB,且所有 SQL 执行路径都被显式 Expect。
模型层必须接收可替换的 *sql.DB 或 Querier 接口
如果模型方法签名是 func GetUser(id int) (*User, error) 且内部自己调用 sql.Open,那根本没法 mock。必须把数据访问依赖显式暴露出来:
- 推荐方式:定义
type Querier interface { QueryRowContext(...); ExecContext(...); QueryContext(...) },让模型方法接收该接口 - 兼容方式:直接接收
*sql.DB参数,测试时传入sqlmock.New()返回的实例 - 避免方式:在模型 struct 里 embed
*sql.DB且没提供注入点——这种代码必须重构才能测
ExpectQuery 和 ExpectExec 必须严格匹配 SQL 字符串
SQLMock 默认不做任何格式归一化,空格、换行、占位符风格(? vs )全算在匹配范围内:
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
- PostgreSQL 用户:确保业务代码用
$1占位符,或初始化 mock 时指定sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual) - MySQL 用户:默认用
?,但写ExpectQuery("SELECT * FROM users WHERE id = ?")时,实际执行的 SQL 不能多一个空格 - 安全做法:用
regexp.QuoteMeta("SELECT * FROM users") + ` WHERE id = \?`构造正则表达式,比纯字符串更鲁棒
返回 rows 时列名和顺序必须与 Scan 或 StructScan 完全对齐
哪怕只差一个字段名大小写,sqlx.StructScan 就会静默跳过字段或 panic:
- 用
mock.NewRows([]string{"id", "name", "created_at"})显式声明列头,顺序必须和Scan(&id, &name, &created_at)一致 - 若结构体 tag 是
db:"user_id",列名就得写"user_id",不能写"id"或"ID" - 时间字段别传字符串,用
sqlmock.NewNullTime(time.Now(), true);否则Scan会因类型不匹配失败 - 空结果集也要调
WillReturnRows(mock.NewRows([]string{"id"})),不能漏掉NewRows调用
事务逻辑必须手动控制 ExpectBegin / ExpectCommit 流程
模型层若封装了事务(比如 tx, _ := db.Begin()),mock 不会自动感知——它只认你注册的期望:
- 先调
mock.ExpectBegin(),再用mockDB.Begin()获取 mock tx - 对
tx.QueryRowContext的调用,必须单独用mock.ExpectQuery(...).WithinTransaction()绑定到该 tx - 没调
mock.ExpectCommit()或mock.ExpectRollback(),tx.Commit()就会 panic - 嵌套事务(如多个
Begin())每个都得配对 Expect,哪怕只是空事务
最易被忽略的是:mock.ExpectationsWereMet() 必须在测试末尾显式调用,且不能被 defer 干扰执行顺序——它不是“收尾装饰”,而是断言核心。一旦漏掉,所有未命中的 Expect 都不会报错,测试看似通过,实则完全没验证 SQL 行为。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










