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

如何使用Golang测试数据库操作_Golang sql包单元测试技巧  第1张

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。别让一种方案包打天下。