应使用 sqlmock 模拟数据库连接以提升测试速度与隔离性,关键包括:用 sqlmock.New() 创建 mock、显式声明每条 SQL 期望、调用 ExpectationsWereMet() 验证;对需真实 DB 的场景,用事务包装并回滚保证数据清洁;避免全局 DB 实例,时间函数需手动 mock。

用 sqlmock 模拟数据库连接,避免真实 DB 依赖
真实数据库会拖慢测试速度、引入环境依赖、导致并发冲突。直接在测试里连 MySQL/PostgreSQL,等于把单元测试写成了集成测试。用 sqlmock 可以拦截所有 database/sql 的调用,只验证 SQL 语句结构、参数绑定、执行顺序是否符合预期。
关键点:
- 必须用
sqlmock.New()创建 mock DB,再传给被测函数(不能在函数内部自己sql.Open) - 每条 SQL 调用都要显式声明期望:比如
mock.ExpectQuery("INSERT").WithArgs("alice", 25) - 测试末尾务必调用
mock.ExpectationsWereMet(),否则即使 SQL 写错了也不会报错
func TestCreateUser(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatal(err)
}
defer db.Close()
mock.ExpectQuery(`INSERT INTO users`).WithArgs("alice", 25).WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(123))
userID, err := CreateUser(db, "alice", 25)
if err != nil {
t.Fatal(err)
}
if userID != 123 {
t.Error("expected user ID 123")
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Error(err)
}
}
用事务包装测试,保证数据隔离与自动回滚
有些逻辑必须走真实数据库(比如触发器、JSON 字段解析、复杂索引行为),这时不能 mock,但又不能让测试污染 DB。最稳妥的做法是:每个测试开启事务 → 执行操作 → 断言 → 回滚。这样既验证了真实 SQL 行为,又不留下脏数据。
注意:
立即学习“go语言免费学习笔记(深入)”;
- PostgreSQL 和 MySQL 都支持事务内建表(
CREATE TEMP TABLE),但 SQLite 更轻量,适合纯内存测试 - 不要用
db.Exec("BEGIN")手动控制,应使用db.Begin()获取*sql.Tx - 回滚必须放在
defer中,且要检查tx.Rollback()返回的 error —— 如果事务已提交,rollback 会报sql.ErrTxDone
func TestUpdateUserEmailInTx(t *testing.T) {
db := setupTestDB() // 连接测试专用 SQLite 或 PostgreSQL
tx, err := db.Begin()
if err != nil {
t.Fatal(err)
}
defer func() {
if r := tx.Rollback(); r != nil && r != sql.ErrTxDone {
t.Error(r)
}
}()
_, err = tx.Exec("INSERT INTO users (name, email) VALUES (?, ?)", "bob", "bob@old.com")
if err != nil {
t.Fatal(err)
}
err = UpdateUserEmail(tx, "bob", "bob@new.com")
if err != nil {
t.Fatal(err)
}
var email string
err = tx.QueryRow("SELECT email FROM users WHERE name = ?", "bob").Scan(&email)
if err != nil {
t.Fatal(err)
}
if email != "bob@new.com" {
t.Error("email not updated")
}
}
测试事务失败回滚是否生效:故意触发错误并查状态
只验证“成功路径”不够。真正容易出问题的是异常分支:比如第二条 SQL 失败时,第一条是否真的没生效?这时候不能只看函数返回 error,得去数据库查最终状态。
实操要点:
- 在事务中执行多步操作,中间某步用
mock.ExpectExec(...).WillReturnError(fmt.Errorf("constraint failed"))模拟失败 - 或者用真实 DB,在第二步故意插入违反唯一键的数据,再查第一条数据是否存在
- 特别注意:SQLite 默认不支持多个活跃事务,测试并发事务需换 PostgreSQL 并用
pgx+pgxpool
避免测试误用全局 *sql.DB 实例
常见错误是把 DB 初始化写在 init() 或包级变量里,导致测试间共享连接和状态。例如一个测试调用了 db.SetMaxOpenConns(1),会影响后续所有测试。
正确做法:
- 所有测试用的
*sql.DB必须在测试函数内创建或通过参数注入 - 如果用
testify/suite,在SetupTest中初始化,TeardownTest中关闭 - 对 SQLite,每次测试用不同内存 DB(
"file::memory:?cache=shared")或临时文件路径
最容易被忽略的一点:SQL 查询中的时间函数(如 NOW()、CURRENT_TIMESTAMP)在 mock 下不会自动替换,必须手动 mock.ExpectQuery("SELECT.*NOW").WillReturnRows(...),否则测试会 panic。










