测试不用真实数据库而选内存数据库,因其启动快、无外部依赖、状态易重置,保障测试快速、稳定、可并行;sqlite的:memory:模式最常用,需每个测试用独立*sql.DB实例防污染。
真实数据库启动慢、依赖外部服务、状态难重置,会导致测试变慢、不稳定、难以并行。内存数据库(如 sqlite 的 :memory: 模式、bbolt 内存模式、或纯内存实现的 go-sqlmock + sqlmock 驱动)能绕过 I/O 和网络,让单元测试真正“快”和“隔离”。
sqlite 的 :memory: 模式做真实 SQL 测试这是最常用也最贴近生产环境的做法:用真实 SQL 驱动跑在内存里,既验证 SQL 逻辑,又避免磁盘/连接开销。关键点是每个测试必须用独立的 *sql.DB 实例,否则事务和表结构会互相污染。
sqlite3.Open("file::memory:?cache=shared") 是基础写法,但注意 cache=shared 可让多个 *sql.DB 共享同一内存数据库(仅限单 goroutine 场景):memory: 实例,并在测试开始时执行建表语句(例如用 db.Exec("CREATE TABLE users(...)"))*sql.DB,否则 TestA 创建的表可能被 TestB 误读——Go 的 testing.T.Parallel() 下尤其危险func TestUserCreate(t *testing.T) {
db, err := sql.Open("sqlite3", "file::memory:")
if err != nil {
t.Fatal(err)
}
defer db.Close()
_, err = db.Exec(`CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)`)
if err != nil {
t.Fatal(err)
}
// 真实业务逻辑调用
err = CreateUser(db, "alice")
if err != nil {
t.Fatal(err)
}
var count int
err = db.QueryRow("SELECT COUNT(*) FROM users").Scan(&count)
if err != nil || count != 1 {
t.Errorf("expected 1 user, got %d", count)
}
}
sqlmock 模拟数据库行为(不执行真实 SQL)适合验证 DAO 层是否发出了预期 SQL,但不关心 SQL 是否真能运行。它不连接任何数据库,纯 mock,因此无法捕获语法错误或约束冲突。
sqlmock.New() 创建 *sql.DB,且不能传给 sql.Open
mock.ExpectQuery() 或 mock.ExpectExec(),否则测试会 panicmock.ExpectationsWereMet() 必须放在 defer 或结尾,否则未触发的期望不会报错func TestUserCreateWithMock(t *testing.T) { db, mock, err := sqlmock.New() if err != nil { t.Fatal(err) } defer db.Close() mock.ExpectExec(`INSERT INTO users`).WithArgs("alice").WillReturnResult(sqlmock.NewResult(1, 1)) err = CreateUser(db, "alice") if err != nil { t.Fatal(err) } if err := mock.ExpectationsWereMet(); err != nil { t.Error(err) } }
内存数据库不是“自动干净”的魔法盒。很多问题源于 Go 的 database/sql 默认行为和 SQLite 驱动细节。
:memory: 数据库在 *sql.DB 关闭后即销毁,但若代码中用了 db.Begin() 却没 tx.Commit() 或 tx.Rollback(),事务会一直挂起,导致后续操作卡住或报 database is locked
sql.Open 不建立连接,首次 db.Query 才真正初始化;如果测试中只 sql.Open 但没执行任何语句,db 实际未生效,容易误判“测试通过”import _ "github.com/mattn/go-sqlite3" 会导致 sql.Open("sqlite3", ...) 报错 sql: unknown driver "sqlite3"
github.com/glebarez/sqlite(纯 Go 实现)替代 cgo 驱动时,路径协议要写成 sqlite://:memory:,且不支持 cache=shared
最易忽略的是:SQLite 的 :memory: 数据库默认是“连接级私有”,哪怕你用同一个 DSN,两个 sql.Open 返回的 *sql.DB 看不到彼此的表——这不是 bug,是设计。需要共享就得显式加 cache=shared,但要注意它不适用于并发测试。