3

I need to test a set of gin handlers with dependencies. I have some successful unit tests up and running, mocking the gin context. I notice, however, that ShouldBindURI never works. The key passed to my repo mock is always empty.

I find this disturbing as it should be failing if it can't bind the key. I suspect this happens because it's unit tests, so I don't have a router telling it where to look for variables in the URL. Is there any way to tweak the gin context to fix this?

Simplified version of what I'm doing

type SomeHandler struct {
  Repo ParticularRepoInterface
  Queue Queuer
  Timeout time.Duration
}

func NewHandler(repo DatabaseInterface, queue Queuer) *SomeHandler {
  return &SomeHandler{
    Repo: repo.ParticularRepo(),
    Queue: queue,
    Timeout:   time.Second * 10,

}

func(ctrl *SomeHandler) List(c *gin.context) {
  ctx, cancelContext := context.WithTimeout(c.Request.Context(), ctrl.Timeout)
  defer cancelContext()

  var key SomeKey

  err := c.ShouldBindUri(&key)
  if err != nil {
    // handle BindError
  }

  thing, err := repo.List(ctx, key)
  if err != nil {
    // handle errors
  }

  c.JSON(http.StatusOk, thing)
}

// followed by the rest of CRUD. 

Tests are unit tests, so I'm mocking the gin context. ParentID is the thing that gets bound in the key which is used to call List.

func TestThing(t *testing.T) {
  t.Parallel()
  assert := assert.New(t)
  gin.SetMode(gin.TestMode)
  now := time.Now().UTC()

  filledThing := Thing{
    ParentID: gofakeit.UUID(),
    ThingID: gofakeit.UUID(),
    Time: &now
  }

  nilThing := Thing{
    ParentID: gofakeit.UUID(),
    ThingID: gofakeit.UUID(),
    Time: nil,
  }
  
  t.Run("Success", func(t *testing.T) {
    t.Parallel()
    var key SomeKey
    parentID := gofakeit.UUID()
    mockThingList := []*Thing{&nilThing, &filledThing}

    mockQueue := NewMockQueue()
    repo := new(mocks.MockThingRepo)
    repo.On("List", mock.AnythingOfType("*context.valueCtx"), key).Return(mockThingList, nil)

    handler := handlers.NewThing(mockRepo, mockQueue)
    url := "/parents/" + parentID + "/things"
    recorder := httptest.NewRecorder()
    request, err := http.NewRequest(http.MethodGet, url, nil)
    assert.NoError(err)
    request.RequestURI = url

    context, _ := gin.CreateTestContext(recorder)
    context.Request = request

    handler.List(context)

    expectedResponse, err := json.Marshal(mockThingList)
    assert.NoError(err)

    // more assert tests
  }

}

  

Laura
  • 288
  • 1
  • 4
  • 14

2 Answers2

1

You should mock a Http request than use gin to handle it, not just use your handler directly.

func TestContext(t *testing.T) {
assert := assert.New(t)

// setup test gin
w := httptest.NewRecorder()
c, e := gin.CreateTestContext(w)

type Parent struct {
    ParentID string
}
handler := func(ctx *gin.Context) {
    var s Parent
    if err := ctx.ShouldBindUri(&s); err != nil {
        ctx.AbortWithError(http.StatusBadRequest, err)
        return
    }
    ctx.JSON(http.StatusOK, s)
}

// register handler to test gin
e.GET("/parents/:ParentID/things", handler)
request, _ := http.NewRequest("GET", "/parents/123/things", nil)
c.Request = request

// execute handler
responseRecorder := httptest.NewRecorder()
e.ServeHTTP(responseRecorder, request)

// assert your result
assert.Equal(http.StatusOK, responseRecorder.Code)
assert.Equal(`{"ParentID":"123"}`, responseRecorder.Body.String())

}

Carlowed
  • 11
  • 2
0

I have successfully figured out how to do this, sans setting up a router.

Two steps, first ShouldBindUri checks the context for params, so add the route variable to the params before calling the controller.

context, _ := gin.CreateTestContext(recorder)
context.Request = request

param := gin.Param{Key: "parent_id", Value: parentID}
context.Params = append(context.Params, param)
controller.Index(context)

second, you need to setup the ginValidator upgrade, or ShouldBindUri will not validate on bind. You should be doing this already on your main server. Link to guide. Adding that to tests is a one-liner - binding.Validator = new(GinValidator)

And now calling the controller directly works as expected.

Laura
  • 288
  • 1
  • 4
  • 14