如何对gin处理程序进行单元测试以正确验证URI参数?

huangapple go评论79阅读模式
英文:

How to unit test gin handlers for correct validation of uri params?

问题

我需要测试一组具有依赖关系的gin处理程序。我已经编写了一些成功的单元测试,模拟了gin上下文。然而,我注意到ShouldBindURI从来没有起作用过。传递给我的存储库模拟的键始终为空。

我觉得这很令人不安,因为如果无法绑定键,它应该失败。我怀疑这是因为这些是单元测试,所以我没有一个路由器告诉它在URL中查找变量的位置。有没有办法调整gin上下文来解决这个问题?

我正在做的简化版本如下:

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 {
    // 处理绑定错误
  }

  thing, err := repo.List(ctx, key)
  if err != nil {
    // 处理错误
  }

  c.JSON(http.StatusOK, thing)
}

// 其他CRUD操作

这些测试是单元测试,所以我正在模拟gin上下文。ParentID是绑定在键中的内容,用于调用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)

    // 更多的断言测试
  })
}
英文:

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
}
}

答案1

得分: 1

你应该模拟一个 HTTP 请求,然后使用 gin 来处理它,而不是直接使用你的处理程序。

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

    // 设置测试用的 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)
    }

    // 注册处理程序到测试用的 gin
    e.GET("/parents/:ParentID/things", handler)
    request, _ := http.NewRequest("GET", "/parents/123/things", nil)
    c.Request = request

    // 执行处理程序
    responseRecorder := httptest.NewRecorder()
    e.ServeHTTP(responseRecorder, request)

    // 断言你的结果
    assert.Equal(http.StatusOK, responseRecorder.Code)
    assert.Equal(`{"ParentID":"123"}`, responseRecorder.Body.String())
}
英文:

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())

}

答案2

得分: 0

我已成功找到了解决方法,无需设置路由器。

有两个步骤,首先,ShouldBindUri会检查上下文中的参数,所以在调用控制器之前,将路由变量添加到参数中。

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

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

其次,你需要升级ginValidator,否则ShouldBindUri在绑定时不会进行验证。你应该已经在主服务器上做了这个操作。链接到指南。将其添加到测试中只需要一行代码 - binding.Validator = new(GinValidator)

现在直接调用控制器应该可以正常工作。

英文:

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.

huangapple
  • 本文由 发表于 2023年2月24日 04:04:48
  • 转载请务必保留本文链接:https://go.coder-hub.com/75549796.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定