testify/mock 不识别 sql.ErrNoRows 是因它只返回显式 Return() 的值,故需手动 .Return(nil, sql.ErrNoRows);判断时应使用 Errors.Is(err, sql.ErrNoRows) 而非 ==。

用 testify/mock 模拟数据库驱动时,为什么总报 sql.ErrNoRows 不生效?
因为 testify/mock 默认不识别 sql.ErrNoRows 这类标准错误,它只认你显式 Return() 的值。如果你在 mock 方法里写 .Return(nil, sql.ErrNoRows),调用方拿到的确实是这个错误;但若漏掉这步、或误写成 .Return(nil, nil),下游逻辑就收不到预期错误。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 所有返回
error的 mock 方法,必须显式Return(..., sql.ErrNoRows)或Return(..., errors.New("xxx")),不能依赖默认值 - 检查被测函数是否用
errors.Is(err, sql.ErrNoRows)判断,而不是err == sql.ErrNoRows—— 后者在 mock 场景下容易失败 - 如果用的是
database/sql原生接口(如*sql.Rows),mock 时需额外实现Next()和Scan()行为,否则rows.Next()可能永远返回true
本地跑测试时连不上 localhost:5432,该不该起 docker Postgres?
应该,但别在每个测试里 docker run 一次。临时容器启动慢、端口冲突、残留数据都会让测试不可靠。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 用
testcontainers-go管理生命周期:在TestMain中统一启停,避免重复创建 - 连接字符串用
container.ConnectionString(ctx)动态获取,不硬编码localhost:5432 - 每次测试前用
TRUNCATE table ... RESTART IDENTITY CAScadE清库,比删库重建快,也避免迁移脚本重复执行 - 如果只是验证 SQL 语法或简单 CRUD,优先用
sqlite内存模式("sqlite://:memory:"),启动零延迟,适合快速反馈
sqlmock 报错 there is no expectation for this query 怎么定位?
说明你执行了一条没被 ExpectQuery() 或 ExpectExec() 捕获的 SQL —— 很可能是日志、健康检查、或 ORM 自动生成的隐式语句(比如 GORM 的 select 1 心跳查询)。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 启用
sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual),关闭模糊匹配,让报错更准 - 在测试 setup 里加
mock.ExpectQuery("SELECT 1").WillReturnRows(sqlmock.NewRows([]string{"1"}))拦截心跳 - 用
mock.ExpectationsWereMet()放在defer里,确保所有期望都被触发,避免漏断言 - 如果用 GORM,记得关掉自动迁移:
gorm.Config{SkipDefaultTransaction: true, DisableForeignKeyConstraintWhenMigrating: true}
测试中用 time.Now() 导致时间相关逻辑不一致怎么办?
直接调用 time.Now() 会让测试依赖系统时钟,尤其涉及过期判断、定时任务模拟时,结果不可控。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 把时间获取封装成可注入的函数变量,例如
var Now = time.Now,测试时替换为固定时间 - 使用
github.com/benbjohnson/clock:构造clock.NewMock(),然后传给 DAO 层,调用clk.Add(24 * time.Hour)快进时间 - 数据库字段如果是
timestamp WITH TIME ZONE,测试插入时别用time.Now().UTC(),改用clk.Now().UTC()保持时钟源一致
测试里最常被忽略的,是事务隔离级别和自动提交行为。postgresql 默认 READ COMMITTED,但 sqlmock 不模拟隔离,而 testcontainers 里的真实 DB 又可能因并发测试相互干扰 —— 这类问题往往要到 CI 环境才暴露。