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

为什么不能直接在测试里连真实数据库
直接用 sql.Open 连生产或本地 MySQL/PostgreSQL 会引入环境依赖、慢、不可靠,且并发跑测试时容易因事务未清理干净导致数据污染或死锁。CI 环境里还可能根本没装数据库服务。
真正可行的路径只有两条:用内存数据库(如 sqlite)做轻量集成测试,或彻底隔离 DB 层——用接口抽象 + mock 实现单元测试。
- 单元测试必须快、确定、无副作用 → 选 mock(比如
gomock或testify/mock) - 想验证 SQL 逻辑是否真能执行、事务是否生效 → 用
sqlite内存模式("sqlite3", ":memory:") - PostgreSQL 特性(如
jsonb、pg_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。









