在听到“测试”这个词时,初级开发人员可能会想到太多的事情。他们大多数时候故意跳过测试,认为这是浪费时间的事情。
当然!测试有时可能很耗时,但成正比地可以拯救生命。
“在别人之前纠正错误总是好的。”
今天,我们将探讨有关使用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
评论(0)