如何在Golang中测试数据库操作_Golang sql测试与事务验证方法

20次阅读

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

如何在Golang中测试数据库操作_Golang sql测试与事务验证方法

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。

text=ZqhQzanResources