12

My project is split into three main components: controllers, services, and models. When a route is queried via the URI, the controllers are called, which then call the services to interact with the models, which then interact with the database via gorm.

I am trying to write unit tests for the controllers, but I'm having a hard time understanding how to properly mock the services layer while mocking the gin layer. I can get a mocked gin context, but I'm not able to mock the service layer within my controller method. Below is my code:

resourceController.go

package controllers

import (
    "MyApi/models"
    "MyApi/services"
    "github.com/gin-gonic/gin"
    "net/http"
)

func GetResourceById(c *gin.Context) {
    id := c.Param("id")
    resource, err := services.GetResourceById(id)

    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"status": http.StatusBadRequest, "message": err})
        return
    } else if resource.ID == 0 {
        c.JSON(http.StatusNotFound, gin.H{"status": http.StatusNotFound, "message": "Resource with id:"+id+" does not exist"})
        return
    }

    c.JSON(http.StatusOK, gin.H{
        "id": resource.ID,
        "data1": resource.Data1,
        "data2": resource.Data2,
    })
}

I want to test that the c.JSON is returning with the proper http status and other data. I need to mock the id variable, err variable, and c.JSON function, but when I try to set the c.JSON function in the test to my new function, I get an error saying Cannot assign to c.JSON. Below is my attempt at writing a test:

resourceController_test.go

package controllers

import (
    "github.com/gin-gonic/gin"
    "github.com/stretchr/testify/assert"
    "net/http/httptest"
    "testing"
)

func TestGetResourceById(t *testing.T) {
    var status int
    var body interface{}
    c, _ := gin.CreateTestContext(httptest.NewRecorder())
    c.JSON = func(stat int, object interface{}) {
        status = stat
        body = object
    }
    GetResourceById(c)
    assert.Equal(t, 4, 4)
}

How do I properly write a unit test to test whether the c.JSON is returning the proper values?

Evan Bloemer
  • 1,051
  • 2
  • 11
  • 31

2 Answers2

14

You cannot modify a method of a type in Go. It is defined and immuatable by the package that defines the type at compile time. This is a design decision by Go. Simply don't do it.

You have already use httptest.NewRecorder() as a mock of gin.Context.ResponseWriter, which will records what is written to the response, including the c.JSON call. However, you need to keep a reference of the httptest.ReponseRecorder and then check it later. Note that you only have a marshalled JSON, so you need to unmarshal it to check content (as both Go map and JSON objects's order does not matter, checking marshalled string's equality is error-prone).

For example,

func TestGetResourceById(t *testing.T) {
    w := httptest.NewRecorder()
    c, _ := gin.CreateTestContext(w)
    GetResourceById(c)
    assert.Equal(t, 200, w.Code) // or what value you need it to be

    var got gin.H
    err := json.Unmarshal(w.Body.Bytes(), &got)
    if err != nil {
        t.Fatal(err)
    }
    assert.Equal(t, want, got) // want is a gin.H that contains the wanted map.
}
leaf bebop
  • 7,643
  • 2
  • 17
  • 27
  • Doesn't this actually make the request instead of mocking the request? – Evan Bloemer Dec 05 '19 at 04:09
  • Also, a `gin.Engine` doesn't have a `Status` or `Body` method. – Evan Bloemer Dec 05 '19 at 04:17
  • @EvanBloemer Sorry, the code was mis-typed and very confusing. Now fixed. `w` should be the value of `httptest.NewRecorder()`. – leaf bebop Dec 05 '19 at 05:36
  • @EvanBloemer It does not go through the framework (routing, parsing request, etc.), so I would say it is mocking the request. It has no network connection behind it, so I would say it is fit as a unit test. – leaf bebop Dec 05 '19 at 05:38
  • 1
    how to pass json as request parameter in this above example? – Amit Upa Mar 27 '20 at 10:41
  • @leafbebop In case anyone else had the issue, w doesn't have a `Status` key. I got it to work with `w.Code` instead of `w.Status` (in the `assert.Equal` function). – KawaLo Jul 14 '20 at 22:36
  • (maybe varies according to version) It should be `w.Body.Bytes()` + `Cannot use '&got' (type *gin.H) as type []byte`. Do you know what you could do instead? – TomDT Jan 15 '21 at 14:51
  • @TomDT It should be `json.Unmarshal(w.Body().Bytes(), &got)`. – leaf bebop Jan 16 '21 at 01:27
  • Mhh, weird. I keep getting an error calling `Body` as if it were a function (I'm actually doing something else entirely, but tried this and didn't work). Ty in any case. – TomDT Jan 16 '21 at 04:12
  • @TomDT my bad. I am really poor at coding without an editor and/or compiler. It should be `json.Unmarshal(w.Body.Bytes(), &got)`. I think I got it right this time. – leaf bebop Jan 16 '21 at 12:13
  • can somebody tell me if i have e.g. result from json marshal something like this: gin.H(gin.H{"data":[]interface {}{map[string]interface {}{"id":1, "title":"basematgroup title 1"}}, "utime":1.617635918e+09}) . How can i create such a response for "want"? – Hsn Apr 05 '21 at 15:39
  • or i just need to check with assert equal only "id" field – Hsn Apr 05 '21 at 15:40
  • @Hsn you can just access the id field like any data, and compare them as comparing any data. I don't understand what is your problem, would you give some more details? – leaf bebop Apr 07 '21 at 12:39
1

Based on the testing section, you can do something like:

func TestGetResourceById(t *testing.T) {
    router := setupRouter()

    w := httptest.NewRecorder()
    req, _ := http.NewRequest("GET", "/GetResourceById", nil)
    router.ServeHTTP(w, req)

    assert.Equal(t, 200, w.Code)
    assert.Equal(t, "your expected output", w.Body.String())
}
Alen Siljak
  • 2,482
  • 2
  • 24
  • 29