首页
Preview

Golang:使用测试gin上下文进行单元测试

在听到“测试”这个词时,初级开发人员可能会想到太多的事情。他们大多数时候故意跳过测试,认为这是浪费时间的事情。

当然!测试有时可能很耗时,但成正比地可以拯救生命。

“在别人之前纠正错误总是好的。”

今天,我们将探讨有关使用Golang和gin进行测试的所有内容。

要点

  • 你应该为你的项目编写单元测试或E2E(端到端)测试吗?
  • 如何开始为Golang中的gin处理程序编写单元测试?
  • 创建测试gin上下文
  • 准备请求

1. 你应该为你的项目编写单元测试或E2E(端到端)测试吗?

答案是“这取决于”。

测试可以是任何类型(任何你喜欢的),如单元测试、E2E测试、集成测试等等……

最终测试的责任是验证代码的当前行为,以防止未来的回归。

什么是单元测试?

它是一段代码块,用于验证一小段代码的行为。在Golang中,这主要是一个函数或类。

为什么你应该编写单元测试?

单元测试检查单个单元(函数或类)的工作情况,并在部署到生产环境之前暴露隐藏的漏洞。

考虑你的代码单元作为砖块,完整的代码作为一堵墙。单元是你的代码的基本构建块。因此,只有单个部分正常工作,整个系统才能正常工作。

什么是E2E测试?

它也是一段代码块,将彻底测试代码(从头到尾),最终涉及多个单元,如函数或类。

例如,为了编写API的E2E测试,你需要使用所需的请求数据调用其端点本身。

为什么你应该编写E2E测试?

E2E测试在确保应用程序用户提供质量用户体验方面发挥重要作用。

由于单元测试已经验证了单个单元的行为,因此E2E测试验证单元是否正确组合以使系统工作。

如果你认为单元测试已经成功验证了某个行为,那么E2E测试就可以选择性地使用。

根据用例,两者都可以混合使用。

2. 如何开始为Golang中的gin处理程序编写单元测试?

要开始进行单元测试,需要使用所需的请求数据调用代码单元(函数或类),并观察其输出。

当你拥有简单函数时,这很容易,比如,

func add(a int, b int) int {
    return a + b
}

但是,直接连接到端点并以URL参数、查询参数、请求正文或标头作为输入的代码块会变得有些困难。

但是,这绝不是不可能的!在本文中,我们将讨论如何使用测试gin上下文来测试使用上下文的gin处理程序(函数)。

让我们开始吧!

3. 创建测试gin上下文

考虑下面的简单函数,你想为它编写一个测试。

func HelloWorld(c *gin.Context) {
     text := c.DefaultQuery("txt", "Hey Go dev!!")
     c.JSON(http.StatusOK, gin.H{"greeting": text})
}


// endpoint
r.GET("/hello", HelloWorld)

HelloWorld函数的参数是gin上下文,无法像传递int或string 😨这样的参数一样简单地传递。

我们必须准备测试gin上下文来测试我们的HelloWorld函数,因为它是一个gin处理程序。

func GetTestGinContext() *gin.Context {
    gin.SetMode(gin.TestMode)

    w := httptest.NewRecorder()
    ctx, _ := gin.CreateTestContext(w)
    ctx.Request = &http.Request{
      Header: make(http.Header)
    }

    return ctx
}

上面的代码正在创建一个测试gin上下文,我们可以在其中测试我们的代码 😎!

gin.SetMode(gin.TestMode)—将gin模式设置为测试模式

gin.CreateTestContext(w)—为运行测试创建测试gin上下文,它将具有httptest.NewRecorder作为参数

你可以更多地了解有关httptest的内容。在这里,httptest.NewRecorder()将负责将响应写入HelloWorld()

在这里,ctx.Request正在配置HTTP标头,它包含在上下文中。

耶!你刚刚创建了一个模拟gin上下文以运行测试。

4. 准备请求

由于我们可以具有不同请求方法的HTTP请求,因此让我们单独配置它们。

从前一步中模拟的gin上下文将作为参数传递给下面的模拟请求。

GET

传递路径参数

func MockJsonGet(c *gin.Context) {
	c.Request.Method = "GET"
	c.Request.Header.Set("Content-Type", "application/json")
	c.Set("user_id", 1)

	// set path params
	c.Params = []gin.Param{
		{
			Key:   "id",
			Value: "1",
		},
	}
}
view raw

使用c.Params来为函数提供路径参数。

MockJsonGet将将获取请求数据包装到上下文中。

例如,如果一个端点是这样的r.get("/users/:id”, GetUserId),则在调用函数时必须有id。但是,由于我们没有调用端点,因此可以像上面的代码中所示,将c.Params与所需的键值对一起传递。

传递查询参数

对于查询参数,我们需要通过在c.Request中添加URL键值来稍微修改上下文模拟,如下面的代码片段所示。

// mock gin context
func GetTestGinContext() *gin.Context {
    gin.SetMode(gin.TestMode)

    w := httptest.NewRecorder()
    ctx, _ := gin.CreateTestContext(w)
    ctx.Request = &http.Request{
      Header: make(http.Header),
      URL:    &url.URL{},
    }

    return ctx
}

// mock GET request 
func MockJsonGet(c *gin.Context) {
	c.Request.Method = "GET"
	c.Request.Header.Set("Content-Type", "application/json")
	c.Set("user_id", 1)

	// set query params
	u := url.Values{}
	u.Add("skip", "5")
	u.Add("limit", "10")
	c.Request.URL.RawQuery = u.Encode()
}

完整代码

// code
func GetUserId(c *gin.Context) {

     fmt.Println(c.Query("foo"))  //will print "bar" while running test
     fmt.Println(c.Param("id"))    // will print "1" while running test

     id, _ := strconv.Atoi(c.Param("id"))
     c.JSON(http.StatusOK, id)
}


// test
func TestGetUserId(t *testing.T) {
     w := httptest.NewRecorder()

     ctx := GetTestGinContext(w)

     //configure path params
     params := []gin.Param{
		{
			Key:   "id",
			Value: "1",
		},
	}

     // configure query params
     u := url.Values{}
     u.Add("foo", "bar")

     MockJsonGet(ctx, params, u)

     GetUserId(ctx)

     assert.EqualValues(t, http.StatusOK, w.Code)

     got, _ := strconv.Atoi(w.Body.String())

     assert.Equal(t, 1, got)
}

//mock gin context
func GetTestGinContext(w *httptest.ResponseRecorder) *gin.Context {
     gin.SetMode(gin.TestMode)

     ctx, _ := gin.CreateTestContext(w)
     ctx.Request = &http.Request{
	 Header: make(http.Header),
	 URL:    &url.URL{},
     }

     return ctx
}

//mock getrequest
func MockJsonGet(c *gin.Context, params gin.Params, u url.Values) {
     c.Request.Method = "GET"
     c.Request.Header.Set("Content-Type", "application/json")
     c.Set("user_id", 1)

     // set path params
     c.Params = params

     // set query params
     c.Request.URL.RawQuery = u.Encode()
}

MockJsonGet中,查询参数和路径参数是可选的。

你将看到,查询参数和路径参数值在GetUserId()中打印。

POST

对于所有请求类型,gin测试上下文保持不变。

func MockJsonPost(c *gin.Context, content interface{}) {
	c.Request.Method = "POST"
	c.Request.Header.Set("Content-Type", "application/json")
	c.Set("user_id", 1)

	jsonbytes, err := json.Marshal(content)
	if err != nil {
		panic(err)
	}

	// the request body must be an io.ReadCloser
	// the bytes buffer though doesn't implement io.Closer,
	// so you wrap it in a no-op closer
	c.Request.Body = io.NopCloser(bytes.NewBuffer(jsonbytes))
}

在这里,内容将具有请求JSON,首先将其转换为字节,然后将其包装为测试gin上下文的请求正文。

PUT

PUT请求模拟将是GET和POST的组合,因为要更新某些内容,我们将需要其唯一ID和更新详细信息。

func MockJsonPut(c *gin.Context, content interface{}, params gin.Params) {
	c.Request.Method = "PUT"
	c.Request.Header.Set("Content-Type", "application/json")
	c.Set("user_id", 1)
	c.Params = params

	jsonbytes, err := json.Marshal(content)
	if err != nil {
		panic(err)
	}

	c.Request.Body = io.NopCloser(bytes.NewBuffer(jsonbytes))
}

我只使用了路径参数,你还可以选择包含查询参数。

DELETE

func MockJsonDelete(c *gin.Context, params gin.Params) {
	c.Request.Method = "DELETE"
	c.Request.Header.Set("Content-Type", "application/json")
	c.Set("user_id", 1)
	c.Params = params
}

删除仅需要路径参数/查询参数,如下面的代码片段所示。

结束语

当你想要测试gin处理程序,但不想在测试期间调用实际端点(E2E测试)时,使用测试gin上下文非常有效。

译自:https://blog.canopas.com/golang-unit-tests-with-test-gin-context-80e1ac04adcd

版权声明:本文内容由TeHub注册用户自发贡献,版权归原作者所有,TeHub社区不拥有其著作权,亦不承担相应法律责任。 如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

点赞(0)
收藏(0)
菜鸟一只
你就是个黄焖鸡,又黄又闷又垃圾。

评论(0)

添加评论