Unit testing GORM with go-sqlmock in Go

Prerequisite

Let’s take simple model Person for example.

Model


type
Person struct {
ID uuid.UUID `gorm:"column:id;primary_key" json:"id"`
Name string `gorm:"column:name" json:"name"`
}

Repository

The repository serves as the wrapped data access layer for the given model with two functions GET and CREATE .

type Repository interface {
Get(id uuid.UUID) (*model.Person, error)
Create(id uuid.UUID, name string) error
}

func (p *repo) Create(id uuid.UUID, name string) error {
person := &model.Person{
ID: id,
Name: name,
}

return p.DB.Create(person).Error
}

func (p *repo) Get(id uuid.UUID) (*model.Person, error) {
person := new(model.Person)

err := p.DB.Where("id = ?", id).Find(person).Error

return person, err
}

Testing Setup

Before dive into how the tests will be implemented. There are few components we have to go through first.

  • suite from testify
  • sql-mock from DATA-DOG

Suite

We use suite of testify to ease testing setup. If you are not yet familiar with suite , checkout the quote from the testify below.

type Suite struct {
suite.Suite
DB *gorm.DB
mock sqlmock.Sqlmock

repository Repository
person *model.Person
}

sql-mock

This is probably the main theme today. Again, we had quoted from DATA-DOG for what sql-mock is.

Testing

Finally we are here for today topic. Let’s talk about how the tests should be written to test our GORM operations step by step.

  • Setup suite
  • Setup a series of Expects of sql statements with sql-mock
  • Invoke functions to be tested
  • Assert the return of the functions are correct
  • Check whetherExpectations of sql-mock were met

Setup suite

We will have our mocked database and repository ready at this stage. It quite similar for the orinary setup process but with sql-mock as the sql driver.

func (s *Suite) SetupSuite() {
var (
db *sql.DB
err error
)

db, s.mock, err = sqlmock.New()
require.NoError(s.T(), err)

s.DB, err = gorm.Open("postgres", db)
require.NoError(s.T(), err)

s.DB.LogMode(true)

s.repository = CreateRepository(s.DB)
}

Test SELECT statement

Remember we have a GET function in our Repository right? To retrieve row in person with given id. Let’s check how to test it.

func (s *Suite) Test_repository_Get() {
var (
id = uuid.NewV4()
name = "test-name"
)

s.mock.ExpectQuery(regexp.QuoteMeta(
`SELECT * FROM "person" WHERE (id = $1)`)).
WithArgs(id.String()).
WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).
AddRow(id.String(), name))

res, err := s.repository.Get(id)

require.NoError(s.T(), err)
require.Nil(s.T(), deep.Equal(&model.Person{ID: id, Name: name}, res))
}
  • Expect SELECT * FROM "person" WHERE (id = $1) to be executed
  • With arg id
  • Return the id and name as the stub of expected person record

Test INSERT statement

Besides GET there is another CREATE function in the Repository .

func (s *Suite) Test_repository_Create() {
var (
id = uuid.NewV4()
name = "test-name"
)

s.mock.ExpectQuery(regexp.QuoteMeta(
`INSERT INTO "person" ("id","name")
VALUES ($1,$2) RETURNING "person"."id"`)).
WithArgs(id, name).
WillReturnRows(
sqlmock.NewRows([]string{"id"}).AddRow(id.String()))

err := s.repository.Create(id, name)

require.NoError(s.T(), err)
}
  • Expect INSERT statement to be exectued
  • With arg id and name
  • Return the id for created row

Check whether Expectations of sql-mock were met

The check is put in the AfterTest section to ensure it is performed after each test case.

func (s *Suite) AfterTest(_, _ string) {
require.NoError(s.T(), s.mock.ExpectationsWereMet())
}

--

--

Love podcasts or audiobooks? Learn on the go with our new app.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store