参考博客:Go单测从零到溜系列1—网络测试 | 李文周的博客 (liwenzhou.com)
单元测试到底是什么?我理解的是,让缺少的东西先用模拟出来的(假的)代替,先凑合着用
原文中有这样一句话:无论我们的代码是作为server端对外提供服务或者还是我们依赖别人提供的网络服务(调用别人提供的API接口)的场景,我们通常都不想在测试过程中真正的建立网络连接。本文就专门介绍如何在上述两种场景下mock网络测试。
这句话有点似懂非懂,但是隐隐约约能够感受到,如果我们在goWeb后端代码,使用apifox或者postman测试的时候,网络测试能够帮助我们节省时间,提高效率。抱着这个想法,我开始了学习单元测试。
接着往下看
这个库是用来测试http请求,现在用gin框架开发的接口,应该都是http请求(网络没有详细学过,此处不是太懂),我们使用httptest这个Go标准库提供的包,可以提高测试效率。
httptest编写测试函数,被测试函数Handler (func c*gin.Context)
编写完测试函数我们能干什么呢?
博客原话:这种情况下我们就可以使用httptest这个工具mock一个HTTP请求和响应记录器,让我们的server端接收并处理我们mock的HTTP请求,同时使用响应记录器来记录server端返回的响应内容。
我的理解是:我们之前测试都是使用的apifox来进行测试,需要先把项目跑起来,然后打开apifox,进行封装请求,然后点击发送按钮发送请求,然后再返回代码进行debug.... 现在的话,我们可以直接跳过apifox,所有的东西都在单元测试代码中完成,这样的话,就省去了使用apifox所花费的时间。
继续往下看:
有一个例子,代码如下
// gin.go
package httptest_demo
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
)
// Param 请求参数
type Param struct {
Name string `json:"name"`
}
// helloHandler /hello请求处理函数
func helloHandler(c *gin.Context) {
var p Param
if err := c.ShouldBindJSON(&p); err != nil {
c.JSON(http.StatusOK, gin.H{
"msg": "we need a name",
})
return
}
c.JSON(http.StatusOK, gin.H{
"msg": fmt.Sprintf("hello %s", p.Name),
})
}
// SetupRouter 路由
func SetupRouter() *gin.Engine {
router := gin.Default()
router.POST("/hello", helloHandler)
return router
}
package httptest_demo
import (
"encoding/json"
"github.com/stretchr/testify/assert"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func Test_helloHandler(t *testing.T) {
tests := []struct {
name string `json:"name"`
param string `json:"param"`
expect string `json:"expect"`
}{
{"base case", `{"name":"liwenzhou"}`, `hello liwenzhou`},
{"bad case", "", "we need a name"},
}
r := SetupRouter()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
//mock一个http请求
req := httptest.NewRequest("POST", "/hello", strings.NewReader(tt.param))
//mock一个响应器记录
w := httptest.NewRecorder()
//让server端口处理mock请求并记录返回的响应内容
r.ServeHTTP(w, req)
//检验状态码是否符合预期
assert.Equal(t, http.StatusOK, w.Code)
//解析并检验响应内容是否符合预期
var resp map[string]string
err := json.Unmarshal([]byte(w.Body.String()), &resp)
assert.Nil(t, err)
assert.Equal(t, tt.expect, resp["msg"])
})
}
}
最初看着上面的代码,最初不太理解,但是自己跟着上面敲一了一遍,然后又debug运行了一遍,才发现,确实就是和上面说的功效一样,能够让我们跳过apifox的步骤,直接在后端就做好了。
上面的单元测试针对的是我们是服务端,别人从我们的服务端拿数据。但是现在有一种新的情景,我们需要去其他的客户端拿数据,也就是说,我们需要去访问其他第三方网站。
这个时候,我们使用gock可以对外部的API进行mock,可以指定参数并返回约定好的响应内容。
gock主要是用来解决:不想在测试过程中真正去发送请求;依赖的外部接口还没有开发完成。
看完这来,还是不太理解,如果我们mock了,那不就是等于没有访问第三方的接口,那我们想要的数据肯定是拿到了。
纸上得来终觉浅,绝知此事要躬行。敲一遍代码亲自体会一下
代码如下:
业务逻辑代码,依赖外部API http://your-api.com/post提供的数据
//api.go
package httptest_demo
import (
"bytes"
"encoding/json"
"io/ioutil"
"net/http"
)
type ReqParam struct {
X int `json:"x"`
}
type Result struct {
Value int `json:"value"`
}
func GetResultByAPI(x,y int) int {
p := &ReqParam{X: x}
b, _ := json.Marshal(p)
resp, err := http.Post("http://your-api.com/post", "application/json", bytes.NewBuffer(b))
if err != nil {
return -1
}
body, _ := ioutil.ReadAll(resp.Body)
var res Result
err = json.Unmarshal(body, &res)
if err != nil {
return -1
}
return res.Value + y
}
单元测试代码,使用gock对外部api进行mock,指定参数返回约定好的响应内容。
// api_test.go
package gock_demo
import (
"testing"
"github.com/stretchr/testify/assert"
"gopkg.in/h2non/gock.v1"
)
func TestGetResultByAPI(t *testing.T) {
defer gock.Off() // 测试执行后刷新挂起的mock
// mock 请求外部api时传参x=1返回100
gock.New("http://your-api.com").
Post("/post").
MatchType("json").
JSON(map[string]int{"x": 1}).
Reply(200).
JSON(map[string]int{"value": 100})
// 调用我们的业务函数
res := GetResultByAPI(1, 1)
// 校验返回结果是否符合预期
assert.Equal(t, res, 101)
// mock 请求外部api时传参x=2返回200
gock.New("http://your-api.com").
Post("/post").
MatchType("json").
JSON(map[string]int{"x": 2}).
Reply(200).
JSON(map[string]int{"value": 200})
// 调用我们的业务函数
res = GetResultByAPI(2, 2)
// 校验返回结果是否符合预期
assert.Equal(t, res, 202)
assert.True(t, gock.IsDone()) // 断言mock被触发
}
刚开始看的时候,不是很理解代码的意思,但是后面才发现
gock.New("http://your-api.com").
Post("/post").
MatchType("json").
JSON(map[string]int{"x": 2}).
Reply(200).
JSON(map[string]int{"value": 200})
gock.New是关键,他能把尚未开发完成的接口(http://your-api.com)给mock出来,也就是说,即使没有http://your-api.com 接口,我们依然可以拿到数据,只不过数据是假的,是我们自己定出来的。