如何使用Golang测试数据库操作_Golang数据库操作测试与模拟方法

2次阅读

不能直接连真实数据库测试,因环境依赖、慢、不可靠且易数据污染;可行方案是:1. 用 sqlite 内存库做轻量集成测试;2. 用接口抽象+mock(如 sqlmock)做单元测试。

如何使用Golang测试数据库操作_Golang数据库操作测试与模拟方法

为什么不能直接在测试里连真实数据库

直接用 sql.Open 连生产或本地 mysql/postgresql 会引入环境依赖、慢、不可靠,且并发跑测试时容易因事务未清理干净导致数据污染或死锁。CI 环境里还可能根本没装数据库服务。

真正可行的路径只有两条:用内存数据库(如 sqlite)做轻量集成测试,或彻底隔离 DB 层——用接口抽象 + mock 实现单元测试。

  • 单元测试必须快、确定、无副作用 → 选 mock(比如 gomocktestify/mock
  • 想验证 SQL 逻辑是否真能执行、事务是否生效 → 用 sqlite 内存模式("sqlite3", ":memory:"
  • PostgreSQL 特性(如 jsonbpg_trgm)无法被 SQLite 替代 → 需搭配 docker-compose 启临时 PG 容器,但只用于少数关键 e2e 测试

用 sqlmock 拦截并断言 SQL 调用

sqlmock 不是 mock 数据库连接,而是包装 *sql.DB,拦截所有 Query/Exec 调用,让你声明“这段代码应该发一条 INSERT,参数是 user1,返回 1 行”,它会在运行时校验是否匹配。

关键点:

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

  • 必须用 sqlmock.New() 创建 mock DB,并传给被测代码(不能直接 sql.Open
  • 每条预期 SQL 都要显式调用 mock.ExpectQuery()mock.ExpectExec(),否则运行时报错 “there is a remaining expectation which was not matched”
  • 参数匹配默认是严格相等,若 SQL 中有 UUID 或时间戳等动态值,要用 sqlmock.AnyArg() 替代

示例片段:

db, mock, _ := sqlmock.New() defer db.Close() <p>mock.ExpectQuery(<code>INSERT INTO users</code>).WithArgs("alice", sqlmock.AnyArg()).WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(123))</p><div class="aritcle_card flexRow">                                                         <div class="artcardd flexRow">                                                                 <a class="aritcle_card_img" href="/xiazai/code/10965" title="成新网络商城购物系统"><img                                                                                 src="https://img.php.cn/upload/webcode/000/000/014/176448060281217.jpg" alt="成新网络商城购物系统"  onerror="this.onerror='';this.src='/static/lhimages/moren/morentu.png'" ></a>                                                                 <div class="aritcle_card_info flexColumn">                                                                         <a href="/xiazai/code/10965" title="成新网络商城购物系统">成新网络商城购物系统</a>                                                                         <p>使用模板与程序分离的方式构建,依靠专门设计的数据库操作类实现数据库存取,具有专有错误处理模块,通过 Email 实时报告数据库错误,除具有满足购物需要的全部功能外,成新商城购物系统还对购物系统体系做了丰富的扩展,全新设计的搜索功能,自定义成新商城购物系统代码功能代码已经全面优化,杜绝SQL注入漏洞前台测试用户名:admin密码:admin888后台管理员名:admin密码:admin888</p>                                                                 </div>                                                                 <a href="/xiazai/code/10965" title="成新网络商城购物系统" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span> </a>                                                         </div>                                                 </div><p>repo := &UserRepository{DB: db} id, _ := repo.Create(context.Background(), "alice")</p><p>assert.NoError(t, mock.ExpectationsWereMet())

用 sqlite 内存数据库做快速集成测试

SQLite 的 :memory: 模式每次新建连接都是全新空库,天然隔离,启动零成本,适合验证 migration、复杂 JOIN、事务回滚等真实行为。

注意几个坑:

  • Go 的 sqlite3 驱动默认不支持 foreign_keys=1,外键约束不会生效 → 初始化时加 _fk=true 参数:sql.Open("sqlite3", "file::memory:?_fk=true")
  • 表名大小写敏感,SQLite 默认小写,而 PostgreSQL 大写自动转小写;如果代码里写了 select * FROM Users,SQLite 会报错找不到表 → 统一用小写建表名
  • 没有 NOW() 函数,得用 datetime('now');自增主键是 Integer PRIMARY KEY,不是 SERIAL

迁移文件若含 PostgreSQL 专属语法(如 CREATE EXTENSION pg_trgm),需为测试单独维护一份 SQLite 兼容版,或用条件编译跳过。

如何设计可测的数据库访问层

核心原则:让 DB 操作函数依赖接口,而非具体 *sql.DB。这样测试时才能注入 mock 或内存 DB。

典型结构:

  • 定义 type Querier Interface { QueryRow(...); Exec(...) },让 repository 接收该接口
  • 生产代码传入 *sql.DB(它实现了 Querier);测试代码传入 sqlmock.Sqlmock 或包装了 *sql.DB 的测试适配器
  • 避免在 repository 方法里调用 db.Begin() —— 事务应由上层(service 层)控制,否则无法在测试中统一 rollback
  • SQL 字符串不要拼接,全部用 ? 占位符,否则 sqlmock 无法做参数匹配

最容易被忽略的一点:事务上下文必须透传。如果 service 层开了 tx, _ := db.Begin(),却把 tx 当作普通 *sql.Tx 传进 repository,那 mock 时就得额外实现 sqlmock.Sqlmock*sql.Tx 的拦截 —— 更简单的方式是让 Querier 接口也包含 Begin() 方法,mock 实现返回一个假 Querier

text=ZqhQzanResources