直接用接口抽象+手写mock可干净隔离数据库操作,无需框架或db实例;硬依赖*sql.db会导致连接真实库或陷入删表循环,残留数据易污染后续用例;go强调依赖接口而非实现,须先封装数据库操作为接口,再通过依赖注入替换实现。

直接用接口抽象 + 手写 Mock 就能干净隔离数据库操作,不需要框架、不启动任何 DB 实例,测试快且稳定。
为什么不能直接在测试里 new *sql.DB
硬依赖 *sql.DB 或具体实现(如 gorm.DB)会让测试被迫连接真实数据库,或陷入“删表→跑测试→再删表”的脆弱循环。更糟的是,一旦测试中途 panic,残留数据可能污染后续用例。Go 的依赖注入哲学是“依赖接口,而非实现”,所以第一步必须把数据库操作封装成接口。
- 不要写
type UserRepository struct { db *sql.DB }—— 这无法替换,mock 成本高 - 要定义类似
type UserRepo interface { Create(*User) error; FindByID(uint) (*User, error) } - 生产代码里用
postgresRepo实现它,测试时换mockRepo
手写 Mock 比 gomock 更轻量可控
对于中小型项目,手写 Mock 足够用,还能精准控制返回逻辑,避免框架生成代码的冗余和黑盒感。关键是让 Mock 支持灵活行为:比如模拟成功、特定错误、空结果、甚至延迟响应。
- 用字段函数闭包,例如
FindFunc func(id uint) (*User, error) - 在测试 setup 阶段赋值:
mock.FindFunc = func(id uint) (*User, error) { return &User{ID: id}, nil } - 支持多次调用不同行为:
mock.CreateFunc = func(u *User) error { calls++; if calls == 2 { return sql.ErrNoRows } else { return nil } } - 别忘了导出字段(首字母大写),否则测试里无法赋值
表驱动测试 + Mock 组合验证边界逻辑
数据库操作的业务逻辑常含条件分支(如“用户不存在则创建”),光测 happy path 不够。用表驱动配合 Mock 可一次性覆盖多种 DB 响应场景。
- 每个测试用例指定 Mock 行为 + 期望输出
- 例如:
{name: "user exists", findRet: &User{ID: 1}, wantErr: false} - 再比如:
{name: "find returns error", findErr: errors.New("timeout"), wantErr: true} - Mock 的行为由用例决定,不跨用例污染状态
- 避免在
TestMain里全局初始化 Mock,每个测试独立构造实例
build tags 隔离集成测试与单元测试代码
当项目同时存在单元测试(Mock)和集成测试(真实 DB)时,容易误跑错环境。用 // +build !integration 和 // +build integration 显式分离文件是最稳妥的做法。
-
user_repo_test.go标记// +build !integration→ 只含 Mock 单元测试 -
user_repo_integration_test.go标记// +build integration→ 启动 Docker 容器或 SQLite 内存 DB - 运行单元测试:
go test -tags="" ./...(默认不启用 integration tag) - 运行集成测试:
go test -tags=integration ./... - CI 中可分别配置两套 job,避免互相干扰
真正难的不是写 Mock,而是判断哪些逻辑值得隔离、哪些该走真实 DB。比如 SQL 拼接、参数校验、事务控制逻辑适合 Mock;而 GORM 钩子执行顺序、外键约束行为、JSONB 字段序列化这类,必须进集成环境验证。别在 Mock 里模拟 PostgreSQL 的 ON CONFLICT DO UPDATE 语义——那已经不是测试你的代码,是在测试你对 PG 的理解是否准确。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











