使用go-sqlmock测试gorm数据库迁移时出现空指针运行时错误
问题概述
我尝试搭配使用go-sqlmock与gorm编写测试用例,目标是测试数据库初始化迁移逻辑,但运行时触发了panic: runtime error: invalid memory address or nil pointer dereference错误,暂未定位到根因。从错误栈判断,触发崩溃的是db.AutoMigrate(&models.User{})这行代码:目前排查确认到该行执行时db实例已正常初始化,传入参数models.User也已正确定义实例化,怀疑问题出在mocks.NewDatabase函数实现中,但暂未找到问题点。
希望大家帮忙查看下述相关代码定位问题,代码中已标注崩溃发生的位置(位于最后两个代码块),如果需要更多上下文可以告知我。
相关代码
project/src/models/models.go
package models import ( "time" "github.com/google/uuid" "gorm.io/gorm" ) type Base struct { ID uuid.UUID `json:"-" gorm:"primaryKey;type:uuid;not null"` CreatedAt time.Time `json:"-" gorm:"autoCreateTime"` UpdatedAt time.Time `json:"-" gorm:"autoUpdateTime"` DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` } type User struct { Base Name string `json:"-"` Email string `json:"-" gorm:"unique_index:user_email_index"` Password string `json:"-" gorm:"size:72"` }
project/src/mocks/database.go
package mocks import ( "project/src/models" "log" "github.com/DATA-DOG/go-sqlmock" "gorm.io/driver/mysql" "gorm.io/gorm" ) func NewDatabase() (*gorm.DB, sqlmock.Sqlmock) { // get db and mock sqlDB, mock, err := sqlmock.New( sqlmock.QueryMatcherOption(sqlmock.QueryMatcherRegexp), ) if err != nil { log.Fatalf("[sqlmock new] %s", err) } defer sqlDB.Close() // create dialector dialector := mysql.New(mysql.Config{ Conn: sqlDB, DriverName: "mysql", }) // a SELECT VERSION() query will be run when gorm opens the database // so we need to expect that here columns := []string{"version"} mock.ExpectQuery("SELECT VERSION()").WithArgs().WillReturnRows( mock.NewRows(columns).FromCSVString("1"), ) // open the database db, err := gorm.Open(dialector, &gorm.Config{ PrepareStmt: true }) if err != nil { log.Fatalf("[gorm open] %s", err) } return db, mock }
project/src/database/init.go
package database import ( "project/src/models" "gorm.io/gorm" ) // Init auto-migrates the DB. func Init(db *gorm.DB) { // Migrate the schema // 此处触发panic // panic: runtime error: invalid memory address or nil pointer dereference // 此处User已正确定义并实例化 db.AutoMigrate(&models.User{}) }
project/src/database/init_test.go(测试用例)
package database import ( "project/src/mocks" "testing" ) func TestInitMigratesDB(t *testing.T) { db, mock := mocks.NewDatabase() mock.ExpectExec("CREATE TABLE users(.*)") mock.ExpectCommit() // 此处执行失败 Init(db) }
错误日志
Running tool: /usr/local/go/bin/go test -timeout 30s -run ^TestInitMigratesDB$ project/src/database --- FAIL: TestInitMigratesDB (0.00s) panic: runtime error: invalid memory address or nil pointer dereference panic: runtime error: invalid memory address or nil pointer dereference [recovered] panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x11ce22e] goroutine 35 [running]: testing.tRunner.func1.2({0x1505320, 0x19bfb00}) /usr/local/go/src/testing/testing.go:1209 +0x24e testing.tRunner.func1() /usr/local/go/src/testing/testing.go:1212 +0x218 panic({0x1505320, 0x19bfb00}) /usr/local/go/src/runtime/panic.go:1038 +0x215 database/sql.(*Rows).close(0x0, {0x0, 0x0}) /usr/local/go/src/database/sql/sql.go:3267 +0x8e database/sql.(*Rows).Close(0x1e) /usr/local/go/src/database/sql/sql.go:3263 +0x1d panic({0x1505320, 0x19bfb00}) /usr/local/go/src/runtime/panic.go:1038 +0x215 database/sql.(*Rows).Next(0x0) /usr/local/go/src/database/sql/sql.go:2944 +0x27 database/sql.(*Row).Scan(0xc0000afbd8, {0xc0000efb38, 0x11, 0x1}) /usr/local/go/src/database/sql/sql.go:3333 +0xb4 gorm.io/gorm/migrator.Migrator.CurrentDatabase({{0x0, 0xc000483350, {0x1659c58, 0xc00041a0f0}}}) /go/pkg/mod/gorm.io/gorm@v1.21.15/migrator/migrator.go:673 +0x8d gorm.io/gorm/migrator.Migrator.HasTable.func1(0xc0000f8380) /go/pkg/mod/gorm.io/gorm@v1.21.15/migrator/migrator.go:265 +0x51 gorm.io/gorm/migrator.Migrator.RunWithValue({{0x80, 0xc000483260, {0x1659c58, 0xc00041a0f0}}}, {0x1512320, 0xc0004fe2a0}, 0xc0000efcb8) /go/pkg/mod/gorm.io/gorm@v1.21.15/migrator/migrator.go:50 +0x126 gorm.io/gorm/migrator.Migrator.HasTable({{0x0, 0xc000483260, {0x1659c58, 0xc00041a0f0}}}, {0x1512320, 0xc0004fe2a0}) /go/pkg/mod/gorm.io/gorm@v1.21.15/migrator/migrator.go:264 +0xe8 gorm.io/gorm/migrator.Migrator.AutoMigrate({{0x0, 0xc000426f90, {0x1659c58, 0xc00041a0f0}}}, {0xc00040f690, 0x0, 0x0}) /go/pkg/mod/gorm.io/gorm@v1.21.15/migrator/migrator.go:92 +0x127 gorm.io/gorm.(*DB).AutoMigrate(0x151a800, {0xc00040f690, 0x1, 0x1}) /go/pkg/mod/gorm.io/gorm@v1.21.15/migrator.go:26 +0x43 project/src/database.Init(0xc00041c230) /Projects/project/src/database/init.go:12 +0x7b project/src/database.TestInitMigratesDB(0x0) /Projects/project/src/database/init_test.go:12 +0x5a testing.tRunner(0xc0003a21a0, 0x15b5328) /usr/local/go/src/testing/testing.go:1259 +0x102 created by testing.(*T).Run /usr/local/go/src/testing/testing.go:1306 +0x35a FAIL project/src/database 0.276s FAIL
解决方案
根因
- 核心错误是
mocks/database.go中错误添加了defer sqlDB.Close()逻辑:defer会在NewDatabase函数返回前执行,导致返回给测试用例的gorm DB实例底层持有的sql连接已经被提前关闭,后续AutoMigrate访问已关闭的连接时触发空指针panic。 - 次要问题是mock的SQL预期不全:
AutoMigrate执行过程中除了SELECT VERSION()、建表语句外,还会执行查询当前数据库名、判断users表是否存在等SQL,现有测试只声明了建表的预期,会导致mock匹配失败。
修复步骤
1. 调整mocks/database.go
删除defer sqlDB.Close()行,调整函数返回值把底层sqlDB实例返回,供测试用例结束后释放资源:
import "database/sql" // 新增引入 func NewDatabase() (*sql.DB, *gorm.DB, sqlmock.Sqlmock) { sqlDB, mock, err := sqlmock.New( sqlmock.QueryMatcherOption(sqlmock.QueryMatcherRegexp), ) if err != nil { log.Fatalf("[sqlmock new] %s", err) } // 删除 defer sqlDB.Close() 这行 dialector := mysql.New(mysql.Config{ Conn: sqlDB, DriverName: "mysql", }) columns := []string{"version"} mock.ExpectQuery("SELECT VERSION()").WithArgs().WillReturnRows( mock.NewRows(columns).FromCSVString("1"), ) db, err := gorm.Open(dialector, &gorm.Config{ PrepareStmt: true }) if err != nil { log.Fatalf("[gorm open] %s", err) } return sqlDB, db, mock }
2. 调整测试用例init_test.go,补全mock预期和资源释放逻辑
func TestInitMigratesDB(t *testing.T) { sqlDB, db, mock := mocks.NewDatabase() // 测试结束后关闭底层连接 defer sqlDB.Close() // 补全AutoMigrate所需的SQL预期 // 预期查询当前数据库名 mock.ExpectQuery("SELECT DATABASE()").WillReturnRows(sqlmock.NewRows([]string{"database"}).AddRow("test")) // 预期查询users表是否存在 mock.ExpectQuery("SHOW TABLES LIKE 'users'").WillReturnRows(sqlmock.NewRows([]string{"tables_in_test"})) // 预期建表语句 mock.ExpectExec("CREATE TABLE `users`.*").WillReturnResult(sqlmock.NewResult(1, 0)) Init(db) // 校验所有预期SQL都被执行 if err := mock.ExpectationsWereMet(); err != nil { t.Errorf("mock预期未全部满足: %s", err) } }
如果不想调整NewDatabase的返回值,也可以直接删除defer sqlDB.Close()后,在测试用例中通过sqlDB, err := db.DB()获取底层sql实例再关闭。
内容的提问来源于stack exchange,提问作者LeviJames
相关产品推荐
相关产品推荐

