Golang测试数据库实战_使用sqlmock模拟DB交互

2次阅读

sqlmock.new() 返回 *sql.db 是为保持业务代码零修改,仅替换底层驱动;expectquery/expectexec 匹配最终发送的sql字符串,事务需显式 expectbegin/expectcommit 或 expectrollback,结尾必须调用 expectationsweremet() 校验。

Golang测试数据库实战_使用sqlmock模拟DB交互

sqlmock.New() 为什么返回 *sql.DB 而不是 mock 对象

因为 sqlmock 的设计目标是「不改业务代码」——它只替换 sql.Open() 返回的 *sql.DB,让测试时所有 db.Query()db.Exec() 等调用自动路由到 mock 行为。你拿到的仍是标准 *sql.DB,只是底层驱动被替换了。

常见错误现象:
• 直接对 sqlmock 实例调用 Query() 报错:它没有这个方法
• 忘记把 mock *sql.DB 注入到被测函数,结果还是连了真实数据库

  • 必须用 sqlmock.New() 创建 mock DB,并传给被测逻辑(比如通过构造函数或参数注入)
  • 不要试图调用 mock.ExpectQuery() 之后再手动执行查询;expect 是声明式断言,执行仍需走 db.Query()
  • mock 对象本身要调用 ExpectationsWereMet() 结尾校验,否则预期未触发也不会报错

ExpectQuery() 和 ExpectExec() 的参数到底匹配什么?

它们匹配的是「最终发送给数据库的 SQL 字符串」,不是 go 源码里的字符串字面量。这意味着:

  • 占位符会被展开(如 ?$1$1),但 ? 风格占位符(mysql)和 $1postgresql)不会自动转换,必须和实际 driver 一致
  • 换行、多余空格、大小写不影响匹配(默认开启正则模糊匹配),但表名/字段名拼写错误会直接失败
  • 如果用了 db.Query("select * FROM users WHERE id = ?", id),expect 里就得写 mock.ExpectQuery("SELECT.*users.*id") 或精确写 mock.ExpectQuery("SELECT * FROM users WHERE id = \?")

性能影响:正则匹配开销极小,但过度宽泛(如 .*)会让断言失去意义,掩盖 SQL 拼写错误。

立即学习go语言免费学习笔记(深入)”;

事务测试中 sqlmock.Transaction() 容易漏掉哪三步?

mock 事务不是自动开启的,必须显式模拟 BEGIN / COMMIT / ROLLBACK 三个动作,缺一不可。否则 tx.Commit() 会 panic 或静默失败。

  • 先调用 mock.ExpectBegin(),对应 db.Begin()
  • 再对返回的 *sql.Tx 调用 ExpectQuery()ExpectExec(),注意此时 SQL 是在事务上下文中执行
  • 最后必须调用 mock.ExpectCommit()mock.ExpectRollback(),否则 ExpectationsWereMet() 会失败

容易踩的坑:
• 忘记 ExpectBegin(),导致 db.Begin() 返回 nil 或 panic
• 在事务里用了未 expect 的 SQL,mock 不知道怎么响应
tx.Rollback() 后又调用了 tx.Commit(),但只 expect 了 rollback —— 这会触发未满足期望错误

测试完不调用 sqlmock.AssertExpectations() 会怎样?

什么都不会报错,但你的测试是假阳性。mock 允许「没被调用的 expect」静默通过,只有调用 mock.AssertExpectations()(或更常见的 mock.ExpectationsWereMet())才会检查是否所有声明的 SQL 行为都被真实触发。

典型场景:
• 你写了 mock.ExpectQuery("SELECT name").WillReturnRows(rows),但被测函数根本没查 name 字段 → 测试仍绿
• 你 expect 了 2 次 INSERT,但实际只执行了 1 次 → 不报错,直到加了断言

  • 务必在测试末尾加 assert.NoError(t, mock.ExpectationsWereMet())(用 testify/assert)或 if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) }
  • 如果用 t.Cleanup() 注册 cleanup 函数,别忘了里面也要调用 ExpectationsWereMet(),否则 defer 时机可能错过

最常被忽略的是:mock 对象生命周期和测试函数绑定,一旦测试函数 return,未验证的 expect 就永远失效了。

text=ZqhQzanResources