I'm trying to use go-sqlmock
with gorm
for testing. I want to write a test for the initial database migration, but I've hit a panic: runtime error: invalid memory address or nil pointer dereference
and I've been having trouble figuring out why. Judging by the error stack, I think it's this statement that does it: db.AutoMigrate(&models.User{})
. I'm not sure why, as db
has allegedly started up successfully by this point and models.User
is defined and instantiated as an argument to db.AutoMigrate
. I have a feeling the error is in the mocks.NewDatabase
function, but I'm at a loss.
Not sure if anyone has the time or will to take a peak at the relevant code and help me out? I've noted in the code where the failures occur (they're in the final two blocks of code). Let me know if any additional context would help.
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
// this panics with
// panic: runtime error: invalid memory address or nil pointer dereference
// User is defined and instantiated here
db.AutoMigrate(&models.User{})
}
Now the test:
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()
// fails here
Init(db)
}
And the log
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/[email protected]/migrator/migrator.go:673 +0x8d
gorm.io/gorm/migrator.Migrator.HasTable.func1(0xc0000f8380)
/go/pkg/mod/gorm.io/[email protected]/migrator/migrator.go:265 +0x51
gorm.io/gorm/migrator.Migrator.RunWithValue({{0x80, 0xc000483260, {0x1659c58, 0xc00041a0f0}}}, {0x1512320, 0xc0004fe2a0}, 0xc0000efcb8)
/go/pkg/mod/gorm.io/[email protected]/migrator/migrator.go:50 +0x126
gorm.io/gorm/migrator.Migrator.HasTable({{0x0, 0xc000483260, {0x1659c58, 0xc00041a0f0}}}, {0x1512320, 0xc0004fe2a0})
/go/pkg/mod/gorm.io/[email protected]/migrator/migrator.go:264 +0xe8
gorm.io/gorm/migrator.Migrator.AutoMigrate({{0x0, 0xc000426f90, {0x1659c58, 0xc00041a0f0}}}, {0xc00040f690, 0x0, 0x0})
/go/pkg/mod/gorm.io/[email protected]/migrator/migrator.go:92 +0x127
gorm.io/gorm.(*DB).AutoMigrate(0x151a800, {0xc00040f690, 0x1, 0x1})
/go/pkg/mod/gorm.io/[email protected]/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
Figured it out. It was a config option: &gorm.Config{ PrepareStmt: true }
. While this works in production, it does not work with sqlmock. Fixed it by changing it to: &gorm.Config{}
.