unit-testinggomockinggo-gormgomock

How to use gomock (or similar) to mock/verify calls to the DB?


Go here, using gorm to or/map to the DB (PSQL).

I have the following code:

package dbstuff

import (
    "errors"

  "github.com/google/uuid"
  "github.com/jinzhu/gorm"
    _ "github.com/jinzhu/gorm/dialects/postgres"
)

type OrderPersister struct {
        db *gorm.DB
}

func (p *OrderPersister) GetOrder(id uuid.UUID) (*Order, error) {
        ret := &Order{}

        err := p.db.Table("orders").Where("order_id = ?", id).Scan(ret).Error
        return ret, err
}

I am trying to write a unit test for it like so:

package dbstuff

import (
    "testing"
  "errors"

  "github.com/stretchr/testify/assert"
)

func TestErrInternalServerError(t *testing.T) {

  // given
  id := uuid.New()
  op := OrderPersister{}

  // when
  order, err := op.GetOrder(id)

  // then
  assert.NotNil(t, order)
  assert.NotNil(t, err)

}

When I run this I get invalid memory address or nil pointer dereference errors because I'm not instantiating a setting a *gorm.DB on my OrderPersister instance. Is there an easy way to mock/stub this out so that my test will confirm we attempted to query the orders table and return the or/mapped results?


Solution

  • I will use testify package to writing unit tests for your code. Instead of using concrete type *gorm.DB, declaring DB interface for OrderPersister struct. Due to we CAN'T mock concrete type and its methods in Go. We need to create a abstract layer - the interface.

    63622995/db/db.go:

    package db
    
    type OrmDBWithError struct {
        OrmDB
        Error error
    }
    
    type OrmDB interface {
        Table(name string) OrmDB
        Where(query interface{}, args ...interface{}) OrmDB
        Scan(dest interface{}) *OrmDBWithError
    }
    

    63622995/main.go:

    package main
    
    import (
        "github.com/google/uuid"
        _ "github.com/jinzhu/gorm/dialects/postgres"
        "github.com/mrdulin/golang/src/stackoverflow/63622995/db"
    )
    
    type Order struct {
        order_id string
    }
    
    type OrderPersister struct {
        DB db.OrmDB
        //DB *gorm.DB
    }
    
    func (p *OrderPersister) GetOrder(id uuid.UUID) (*Order, error) {
        ret := &Order{}
    
        err := p.DB.Table("orders").Where("order_id = ?", id).Scan(ret).Error
        return ret, err
    }
    

    Created mock object for db which implements the OrmDB interface. Then, you can create and pass this mock DB object to OrderPersister struct.

    63622995/mocks/db.go:

    package mocks
    
    import (
        "github.com/mrdulin/golang/src/stackoverflow/63622995/db"
        "github.com/stretchr/testify/mock"
    )
    
    type MockedOrmDB struct {
        mock.Mock
    }
    
    func (s *MockedOrmDB) Table(name string) db.OrmDB {
        args := s.Called(name)
        return args.Get(0).(db.OrmDB)
    }
    
    func (s *MockedOrmDB) Where(query interface{}, args ...interface{}) db.OrmDB {
        arguments := s.Called(query, args)
        return arguments.Get(0).(db.OrmDB)
    }
    
    func (s *MockedOrmDB) Scan(dest interface{}) *db.OrmDBWithError {
        args := s.Called(dest)
        return args.Get(0).(*db.OrmDBWithError)
    }
    

    63622995/main_test.go:

    package main
    
    import (
        "testing"
    
        "github.com/google/uuid"
        "github.com/mrdulin/golang/src/stackoverflow/63622995/db"
        "github.com/mrdulin/golang/src/stackoverflow/63622995/mocks"
        "github.com/stretchr/testify/assert"
        "github.com/stretchr/testify/mock"
    )
    
    func TestOrderPersister_GetOrder(t *testing.T) {
        assert := assert.New(t)
        t.Run("should get order", func(t *testing.T) {
            testDb := new(mocks.MockedOrmDB)
            id := uuid.New()
            testDb.
                On("Table", "orders").
                Return(testDb).
                On("Where", "order_id = ?", mock.Anything).
                Return(testDb).
                On("Scan", mock.Anything).Run(func(args mock.Arguments) {
                ret := args.Get(0).(*Order)
                ret.order_id = "123"
            }).
                Return(&db.OrmDBWithError{Error: nil})
            op := OrderPersister{DB: testDb}
            got, err := op.GetOrder(id)
            testDb.AssertExpectations(t)
            assert.Nil(err)
            assert.Equal(Order{order_id: "123"}, *got)
        })
    
        t.Run("should return error", func(t *testing.T) {
            testDb := new(mocks.MockedOrmDB)
            id := uuid.New()
            testDb.
                On("Table", "orders").
                Return(testDb).
                On("Where", "order_id = ?", mock.Anything).
                Return(testDb).
                On("Scan", mock.Anything).
                Return(&db.OrmDBWithError{Error: errors.New("network")})
            op := OrderPersister{DB: testDb}
            got, err := op.GetOrder(id)
            testDb.AssertExpectations(t)
            assert.Equal(Order{}, *got)
            assert.Equal(err.Error(), "network")
        })
    }
    

    unit test results:

    === RUN   TestOrderPersister_GetOrder
    === RUN   TestOrderPersister_GetOrder/should_get_order
        TestOrderPersister_GetOrder/should_get_order: main_test.go:32: PASS:    Table(string)
        TestOrderPersister_GetOrder/should_get_order: main_test.go:32: PASS:    Where(string,string)
        TestOrderPersister_GetOrder/should_get_order: main_test.go:32: PASS:    Scan(string)
    === RUN   TestOrderPersister_GetOrder/should_return_error
        TestOrderPersister_GetOrder/should_return_error: main_test.go:49: PASS: Table(string)
        TestOrderPersister_GetOrder/should_return_error: main_test.go:49: PASS: Where(string,string)
        TestOrderPersister_GetOrder/should_return_error: main_test.go:49: PASS: Scan(string)
    --- PASS: TestOrderPersister_GetOrder (0.00s)
        --- PASS: TestOrderPersister_GetOrder/should_get_order (0.00s)
        --- PASS: TestOrderPersister_GetOrder/should_return_error (0.00s)
    PASS
    
    Process finished with exit code 0
    

    Coverage report:

    enter image description here