Search code examples
unit-testinggointegration-testinggo-gin

How to unit test a Go Gin handler function?


I have a controller function like this....

func GetMaterialByFilter(c *gin.Context) {

    queryParam := weldprogs.QueryParam{}
    c.BindQuery(&queryParam)
    materialByFilter, getErr := services.WeldprogService.GetMaterialByFilter(&queryParam)
    if getErr != nil {
        //TODO : Handle user creation error
        c.JSON(getErr.Status, getErr)
        return
    }
    c.JSON(http.StatusOK, materialByFilter)

}

QueryParam Struct is like this..

type QueryParam struct {
    Basematgroup_id []string `form:"basematgroup_id"`
    License_id      []string `form:"license_id"`
    Diameter_id     []string `form:"diameter_id"`
    Gasgroup_id     []string `form:"gasgroup_id"`
    Wiregroup_id    []string `form:"wiregroup_id"`
    Wiremat_id      []string `form:"wiremat_id"`
}

My test function is like this..

func TestGetMaterialByFilter(t *testing.T) {
    w := httptest.NewRecorder()
    c, _ := gin.CreateTestContext(w)
    GetMaterialByFilter(c)
    assert.Equal(t, 200, w.Code) 

    var got gin.H
    err := json.Unmarshal(w.Body.Bytes(), &got)
    if err != nil {
        t.Fatal(err)
    }
    fmt.Println(got)
    assert.Equal(t, got, got) 
}

On running this test it is giving me the following error

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=0x10 pc=0x97f626]

But when i comment out the c.BindQuery() line in my controller function it successfully run my test function. What i am doing wrong here? can i somehow mock the c.BindQuery function?


Solution

  • To test operations that involve the HTTP request, you have to actually initialize an *http.Request and set it to the Gin context. To specifically test c.BindQuery it's enough to properly initialize the request's URL and URL.RawQuery:

    func mockGin() (*gin.Context, *httptest.ResponseRecorder) {
        w := httptest.NewRecorder()
        c, _ := gin.CreateTestContext(w)
    
        // test request, must instantiate a request first
        req := &http.Request{
            URL:    &url.URL{},
            Header: make(http.Header), // if you need to test headers
        }
        // example: req.Header.Add("Accept", "application/json")
    
        // request query
        testQuery := weldprogs.QueryParam{/* init fields */}
    
        q := req.URL.Query()
        for _, s := range testQuery.Basematgroup_id {
            q.Add("basematgroup_id", s)
        }
        // ... repeat for other fields as needed
    
        // must set this, since under the hood c.BindQuery calls
        // `req.URL.Query()`, which calls `ParseQuery(u.RawQuery)`
        req.URL.RawQuery = q.Encode()
        
        // finally set the request to the gin context
        c.Request = req
    
        return c, w
    }
    

    If you need to mock JSON binding, see this answer.


    The service call services.WeldprogService.GetMaterialByFilter(&queryParam) can't be tested as is. To be testable it has to be (ideally) an interface and somehow injected as dependency of your handler.

    Assuming that it is already an interface, to make it injectable, you either require it as an handler argument — but this forces you to change the signature of the handler —, or you set it as a Gin context value:

    func GetMaterialByFilter(c *gin.Context) {
        //...
        weldprogService := mustGetService(c)
        materialByFilter, getErr := weldprogService.GetMaterialByFilter(&queryParam)
        // ...
    }
    
    func mustGetService(c *gin.Context) services.WeldprogService {
        svc, exists := c.Get("svc_context_key")
        if !exists {
            panic("service was not set")
        }
        return svc.(services.WeldprogService)
    }
    

    Then you can mock it in your unit tests:

    type mockSvc struct {
    }
    
    // have 'mockSvc' implement the interface 
    
    func TestGetMaterialByFilter(t *testing.T) {
        w := httptest.NewRecorder()
        c, _ := gin.CreateTestContext(w)
    
        // now you can set mockSvc into the test context
        c.Set("svc_context_key", &mockSvc{})
    
        GetMaterialByFilter(c)
        // ... 
    }