Search code examples
gogo-gormgo-gingo-sqlmock

Issue with go-sqlmock testing in the expected query part


I am using go-sqlmock for the first time and I am trying to write a test for post operation. I am using gorm and gin.

  1. The test is giving me an error where s.mock.ExpectQuery(regexp.QuoteMeta(.... I am not what is the issue here. I have posted both the test and the output.
  2. Also, (this has nothing to do with 1) in this test I really do not know what the code will be as it is randomly generated in the api controller. Is there a way to assign a generic number in the code field.

The test file

     package unit
    
     import (
        "net/http"
        "net/http/httptest"
        "regexp"
        "testing"
    
        "github.com/DATA-DOG/go-sqlmock"
        "github.com/SamiAlsubhi/go/controllers"
        "github.com/SamiAlsubhi/go/routes"
        "github.com/gin-gonic/gin"
        "github.com/stretchr/testify/assert"
        "github.com/stretchr/testify/require"
        "github.com/stretchr/testify/suite"
        "gorm.io/driver/postgres"
        "gorm.io/gorm"
     )
    
    type Suite struct {
        suite.Suite
        DB     *gorm.DB
        mock   sqlmock.Sqlmock
        router *gin.Engine
    }
    
    func (s *Suite) SetupSuite(t *testing.T) {
        conn, mock, err := sqlmock.New()
        if err != nil || conn == nil {
            t.Errorf("Failed to open mock sql db, got error: %v", err)
        }
        s.mock = mock
    
        dialector := postgres.New(postgres.Config{
            DSN:                  "sqlmock_db_0",
            DriverName:           "postgres",
            Conn:                 conn,
            PreferSimpleProtocol: true,
        })
    
        if db, err := gorm.Open(dialector, &gorm.Config{}); err != nil || db == nil {
            t.Errorf("Failed to open gorm v2 db, got error: %v", err)
        } else {
            s.DB = db
        }
        api := &controllers.API{Db: s.DB}
    
        s.router = routes.SetupRouter(api)
    
    }
    
    func TestSetup(t *testing.T) {
        suite.Run(t, new(Suite))
    
    }
    
    func (s *Suite) AfterTest(_, _ string) {
        require.NoError(s.T(), s.mock.ExpectationsWereMet())
    }
    
    func (s *Suite) Test_GetOTP() {
        var (
            phone = "99999999"
            code  = "123456"
        )
    
        s.mock.ExpectQuery(regexp.QuoteMeta(
            `INSERT INTO "otps" ("phone","code") VALUES ($1,$2) RETURNING "otps"."id"`)).
            WithArgs(phone, code).
            WillReturnRows(sqlmock.NewRows([]string{"id"}).
                AddRow(1))
    
        s.mock.ExpectCommit()
    
        w := httptest.NewRecorder()
        req, err := http.NewRequest("GET", "/api/auth/get-otp/"+phone, nil)
        require.NoError(s.T(), err)
    
        s.router.ServeHTTP(w, req)
        assert.Equal(s.T(), 200, w.Code)
    
        //require.Nil(s.T(), deep.Equal(&model.Person{ID: id, Name: name}, w.Body))
    }

the output.

    --- FAIL: TestSetup (0.00s)
        --- FAIL: TestSetup/Test_GetOTP (0.00s)
            /Users/sami/Desktop/SamiAlsubhi/go/test/unit/suite.go:63: test panicked: runtime error: invalid memory address or nil pointer dereference
                goroutine 26 [running]:
                runtime/debug.Stack()
                    /usr/local/go/src/runtime/debug/stack.go:24 +0x65
                github.com/stretchr/testify/suite.failOnPanic(0xc000001a00)
                    /Users/sami/Desktop/golang/pkg/mod/github.com/stretchr/[email protected]/suite/suite.go:63 +0x3e
                panic({0x49e96a0, 0x5193810})
                    /usr/local/go/src/runtime/panic.go:1038 +0x215
                github.com/SamiAlsubhi/go/test/unit.(*Suite).AfterTest(0x4abe61b, {0x4becfd0, 0xc000468940}, {0x0, 0x0})
                    /Users/sami/Desktop/SamiAlsubhi/go/test/unit/setup_test.go:60 +0x1c
                github.com/stretchr/testify/suite.Run.func1.1()
                    /Users/sami/Desktop/golang/pkg/mod/github.com/stretchr/[email protected]/suite/suite.go:137 +0x1b7
                panic({0x49e96a0, 0x5193810})
                    /usr/local/go/src/runtime/panic.go:1038 +0x215
                github.com/SamiAlsubhi/go/test/unit.(*Suite).Test_GetOTP(0xc000468940)
                    /Users/sami/Desktop/SamiAlsubhi/go/test/unit/setup_test.go:69 +0x4f
                reflect.Value.call({0xc000049140, 0xc000010308, 0x13}, {0x4abf50c, 0x4}, {0xc000080e70, 0x1, 0x1})
                    /usr/local/go/src/reflect/value.go:543 +0x814
                reflect.Value.Call({0xc000049140, 0xc000010308, 0xc000468940}, {0xc0003c9e70, 0x1, 0x1})
                    /usr/local/go/src/reflect/value.go:339 +0xc5
                github.com/stretchr/testify/suite.Run.func1(0xc000001a00)
                    /Users/sami/Desktop/golang/pkg/mod/github.com/stretchr/[email protected]/suite/suite.go:158 +0x4b6
                testing.tRunner(0xc000001a00, 0xc000162000)
                    /usr/local/go/src/testing/testing.go:1259 +0x102
                created by testing.(*T).Run
                    /usr/local/go/src/testing/testing.go:1306 +0x35a
    FAIL
    coverage: [no statements]
    FAIL    github.com/SamiAlsubhi/go/test/unit 0.912s
    FAIL

Solution

  • Solution to the first issue:
    when using testify/suite, There are bunch of methods if created for the Suite struct, they will be automatically executed when running the test. That being said, These methods will pass through an interface filter. In the case of .SetupSuite, it has to have NO arguments and No return, in order to run.

    Solution to the second issue: There is a way in go-sqlmock to match any kind of data by using sqlmock.AnyArg().

    Fixed code:

        package unit
        
        import (
            "encoding/json"
            "fmt"
            "math/rand"
            "net/http"
            "net/http/httptest"
            "regexp"
            "testing"
        
            "github.com/DATA-DOG/go-sqlmock"
            "github.com/SamiAlsubhi/go/controllers"
            "github.com/SamiAlsubhi/go/routes"
            "github.com/gin-gonic/gin"
            "github.com/stretchr/testify/assert"
            "github.com/stretchr/testify/require"
            "github.com/stretchr/testify/suite"
            "gorm.io/driver/postgres"
            "gorm.io/gorm"
        )
        
        type Suite struct {
            suite.Suite
            DB     *gorm.DB
            mock   sqlmock.Sqlmock
            router *gin.Engine
        }
        
        func (s *Suite) SetupSuite() {
            //t.Logf("setup start")
            conn, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherRegexp))
            if err != nil || conn == nil {
                panic(fmt.Sprintf("Failed to open mock sql db, got error: %v", err))
            }
            s.mock = mock
        
            dialector := postgres.New(postgres.Config{
                DSN:                  "sqlmock_db_0",
                DriverName:           "postgres",
                Conn:                 conn,
                PreferSimpleProtocol: true,
            })
        
            if db, err := gorm.Open(dialector, &gorm.Config{SkipDefaultTransaction: true}); err != nil || db == nil {
                panic(fmt.Sprintf("Failed to open gorm v2 db, got error: %v", err))
            } else {
                s.DB = db
            }
        
            api := &controllers.API{Db: s.DB, IsTesting: true}
            s.router = routes.SetupRouter(api)
        
        }
        
        func TestSetup(t *testing.T) {
        
            suite.Run(t, new(Suite))
        
        }
        
        // func (s *Suite) AfterTest(_, _ string) {
        //  require.NoError(s.T(), s.mock.ExpectationsWereMet())
        // }
        
        func (s *Suite) Test_GetOTP_Non_Existing_Phone() {
            /*
                This to test getting OTP for a phone number that does not exist in the otps table
            */
            phone := fmt.Sprintf("%v", 90000000+rand.Intn(99999999-90000000))
            s.mock.MatchExpectationsInOrder(false)
        
            s.mock.ExpectQuery(regexp.QuoteMeta(`SELECT count(*) FROM "otps" WHERE phone = $1 AND "otps"."deleted_at" IS NULL`)).
                WithArgs(phone).
                WillReturnRows(sqlmock.NewRows([]string{"count"}).
                    AddRow(0))
        
            s.mock.ExpectQuery(regexp.QuoteMeta(
                `INSERT INTO "otps" ("created_at","updated_at","deleted_at","phone","code") VALUES ($1,$2,$3,$4,$5) RETURNING "id"`)).
                WithArgs(sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), phone, sqlmock.AnyArg()).
                WillReturnRows(sqlmock.NewRows([]string{"id"}).
                    AddRow(1))
        
            w := httptest.NewRecorder()
            req, err := http.NewRequest("GET", "/api/auth/get-otp/"+phone, nil)
            require.NoError(s.T(), err)
        
            s.router.ServeHTTP(w, req)
            assert.Equal(s.T(), 200, w.Code)
            //parse response
            var response gin.H
            err = json.Unmarshal(w.Body.Bytes(), &response)
            require.NoError(s.T(), err)
            _, ok := response["expiry_in"]
            assert.True(s.T(), ok)
        
            require.NoError(s.T(), s.mock.ExpectationsWereMet())
        
        }