应根据测试目标选择 sqlmock(验证 SQL 逻辑)或 SQLite 内存模式(验证端到端行为):前者纯内存模拟、速度快、需设 ExpectQuery/Exec 并调用 ExpectationsWereMet;后者真实执行、需手动建表、适合测迁移、映射和事务。

Go 的 database/sql 本身不提供内存数据库或自动 mock 能力,直接测真实数据库容易污染、慢、不可靠;真正可行的方案是用 sqlmock 模拟驱动行为,或用轻量级真实 DB(如 SQLite 内存模式)做集成验证——二者适用场景不同,选错就卡在 CI 或测试稳定性上。
用 sqlmock 拦截并断言 SQL 执行逻辑
sqlmock 是最常用的测试工具,它替换掉 sql.DB 的底层驱动,让你能精确控制返回结果、检查 SQL 是否被调用、参数是否匹配。它不执行真实 SQL,纯内存模拟,速度快、隔离性好。
常见错误现象:忘记调用 mock.ExpectQuery() 或 mock.ExpectExec() 就执行语句,导致 panic 报 “there is no expectation for…”;或用了 QueryRow().Scan() 却只设了 ExpectQuery(),漏掉 WillReturnRows()。
- 必须先通过
sqlmock.New()创建 mock DB,再传给待测函数(不能直接在函数里sql.Open("postgres", ...)) - 查询类操作用
mock.ExpectQuery("SELECT").WithArgs(...).WillReturnRows(...) - 写入类操作用
mock.ExpectExec("INSERT").WithArgs(...).WillReturnResult(sqlmock.NewResult(1, 1)) - 测试结束后务必调用
mock.ExpectationsWereMet(),否则未触发的 expect 不报错
func TestGetUser(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatal(err)
}
defer db.Close()
mock.ExpectQuery(`^SELECT id, name FROM users WHERE id = \?$`).WithArgs(123).
WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(123, "alice"))
user, err := GetUser(db, 123)
if err != nil {
t.Fatal(err)
}
if user.Name != "alice" {
t.Error("expected alice")
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Error(err)
}
}
用 SQLite 内存模式做端到端行为验证
当你要验证 migration 是否生效、GORM struct tag 是否正确映射、或事务嵌套逻辑时,sqlmock 太薄——它不解析 SQL,也不校验字段类型。这时用 sqlite3 的 :memory: 模式更合适:真实执行 SQL,但进程退出即销毁,无副作用。
立即学习“go语言免费学习笔记(深入)”;
使用场景:测试 DAO 层完整流程、验证外键/索引/默认值、调试“为什么 Scan() 总是 nil”这类底层行为问题。
- 注册驱动:
import _ "github.com/mattn/go-sqlite3" - 打开连接:
db, err := sql.Open("sqlite3", ":memory:") - 必须手动建表(
CREATE TABLE),sqlmock不需要这步,但 SQLite 需要 - 注意 SQLite 的类型亲和性(比如
INT列也能存字符串),和 PostgreSQL/MySQL 行为不一致,别拿它测精度敏感逻辑
避免在测试中硬编码数据库连接字符串
本地跑得通、CI 失败,十有八九是测试代码里写了 sql.Open("pgx", "host=localhost...")。这种写法让测试强依赖外部服务状态,且无法统一管理凭证或超时。
正确做法是把 *sql.DB 作为参数注入,由测试用例决定用 mock 还是真实 DB:
- DAO 函数签名应为
func CreateUser(db *sql.DB, u User) error,而非内部自己sql.Open - 测试时统一用
testDB := setupTestDB(t)工厂函数,内部根据环境变量切换 mock / sqlite / pg - 若用 GORM,记得在测试中禁用日志:
gorm.Config{Logger: logger.Default.LogMode(logger.Silent)},否则大量输出干扰断言
事务测试必须显式 Commit/Rollback
很多人写事务测试时只调 db.Begin(),然后执行操作,却忘了 tx.Commit() 或 tx.Rollback()。这会导致连接泄漏、后续测试失败,甚至 mock 报 “transaction has already been committed”。
尤其注意嵌套事务(如使用 sql.Tx 传参的 service 层):
- 每个
Begin()必须配对Commit()或Rollback(),哪怕测试失败也要 defer 确保执行 - 用
mock.ExpectBegin()+mock.ExpectCommit()可验证事务边界是否被正确开启/提交 - SQLite 内存模式中,事务行为接近真实 DB,适合验证
Savepoint和回滚粒度
最难的不是写测试,而是判断该用 mock 还是真实 DB——查 SQL 语法是否拼错?用 sqlmock;查 JOIN 结果字段顺序是否错乱?用 SQLite;查分布式事务下锁等待是否触发超时?那就得上真实的 PostgreSQL 并配好 pg_ctl。别让一种方案包打天下。










