直接用 sqlite3 的 :memory: 模式 + gin.TestMode 是 Gin 单元测试配 SQLite 最轻、最稳的方案,每个测试函数获得全新空库,事务回滚后彻底干净,避免文件锁、残留数据和跨平台路径问题。

直接用 sqlite3 的 :memory: 模式 + gin.TestMode,就是 Gin 单元测试里配 SQLite 最轻、最稳的路。它不拉容器、不占端口、不写磁盘,每个测试函数拿到的都是全新空库,事务回滚后彻底干净——比 mock 真实,比本地 MySQL 可靠。
为什么不能直接连文件型 SQLite 数据库做单元测试
常见错误是测试里硬编码 "test.db" 路径,然后反复 os.Remove 清库。这会导致:
- 并发测试(
go test -p=4)时多个 goroutine 同时读写一个文件,SQLite 报database is locked -
os.Remove失败(Windows 上文件被占用)、漏删残留表、外键约束导致DROP TABLE失败 - 测试失败后
test.db还在磁盘上,下次跑测试直接继承脏数据,行为不可复现 - CI 环境里路径权限问题、不同 OS 路径分隔符差异(
\vs/)
如何正确初始化 :memory: SQLite 并注入 Gin Handler
关键不是“打开数据库”,而是“每次测试都 new 一个干净的 *sql.DB 实例,并传给 Handler 所依赖的服务层”。
- 用
sql.Open("sqlite3", ":memory:")创建连接,不带任何路径参数 - 立刻执行
db.Exec("PRAGMA foreign_keys = ON")开启外键支持(否则迁移或关联操作静默失败) - 在测试函数内调用你的初始化函数(比如
NewUserService(db)),确保 Handler 用的是这个实例 - 别把
*sql.DB做成包级全局变量——那会跨测试污染
示例片段:
func TestCreateUser(t *testing.T) {
db, _ := sql.Open("sqlite3", ":memory:")
defer db.Close()
db.Exec("PRAGMA foreign_keys = ON")
userSvc := NewUserService(db) // 注入到业务逻辑
router := gin.New()
router.POST("/users", func(c *gin.Context) {
// 这里用 userSvc.Create(...)
})
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/users", strings.NewReader(`{"name":"a"}`))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
}
测试中如何安全执行 SQL 迁移和预置数据
SQLite 的 :memory: 是纯内存的,进程退出就丢。所以迁移必须在每个测试里重跑,不能只做一次。
- 用
github.com/golang-migrate/migrate/v4配合io/fs内存文件系统(如migrate.NewMigrator(db, &sqlite3.Driver{}, migrationsFS)) - 更简单做法:把建表语句写成字符串,在测试开头
db.Exec(createUserTableSQL)——适合小项目,无额外依赖 - 预置测试数据别用
INSERT INTO ... SELECT,直接db.Exec("INSERT INTO users VALUES (...)", ...),避免依赖已有数据状态 - 如果用了 GORM/Sqlx 等 ORM,务必确认其 AutoMigrate 或
CreateTable方法支持:memory:(GORM v2+ 支持,v1 不稳定)
容易被忽略的坑:事务隔离与时间戳字段
:memory: 模式下 SQLite 默认不开启 WAL 模式,多语句执行时若没显式事务包裹,可能触发隐式事务,导致测试间意外可见未提交变更(极少见但存在)。更实际的问题是:
- SQLite 没有原生
TIMESTAMP WITH TIME ZONE,datetime('now')返回的是本地时区时间,测试断言created_at字段易因时区抖动失败 → 改用datetime('now', 'utc')并统一用 UTC 解析 - Handler 中若调用了
time.Now()再写入 DB,测试无法控制时间点 → 应把时间生成逻辑抽成接口(如Clock.Now() time.Time),测试时 fake 掉 -
db.QueryRow("SELECT last_insert_rowid()")在 :memory: 下有效,但若你在迁移里用了AUTOINCREMENT,首次插入后 ID 是 1,这点可安全断言


















