暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

在Gin框架中进行单元测试

猿武场 2022-04-05
2078

「 Go DEV 」这是 Go 语言的时代 

  作者 | uuapp

  整理 | 猿胖子

  出品 | 猿武场(ID:apesarena)

关注公众号并回复数字「 1024 」加入猿武场微信社群 

Gin 框架进行单元测试

单元测试(Unit Tests, UT) 是一个优秀项目不可或缺的一部分,大型项目中尤为重要。

这句话曾是公认的消费产品金科玉律,放在手机这一产品的语境中,就是首发购买能最快尝鲜,体验到更好的性能、拍照、网络质量等,像 iPhone 等极少数产品甚至在首发时还会形成溢价。
想首发拿到的人太多了,首批产品数量不足,供需不匹配,在自由市场内二次买卖涨价,几乎是惯例。
如何写好单元测试呢?
首先,学会写测试用例。比如如何测试单个函数/方法;比如如何做基准测试;比如如何写出简洁精炼的测试代码;再比如遇到数据库访问等的方法调用时,如何  mock

然后,写可测试的代码。高内聚,低耦合
是软件工程的原则,同样,对测试而言,函数/方法写法不同,测试难度也是不一样的。职责单一,参数类型简单,与其他函数耦合度低的函数往往更容易测试。我们经常会说,“这种代码没法测试”,这种时候,就得思考函数的写法可不可以改得更好一些。为了代码可测试而重构是值得的。

接下来将介绍如何使用 Go 语言的标准库 testing
 结合 gin
框架进行单元测试。

一个简单的示例项目

该项目创建一个http服务,使用Sonyflake 算法用于生成全局 id,项目目录结构如下:

 1exampleUt/
2   routers/
3      |-- router.go  // 路由初始化
4   services/
5      |-- globalIdService.go  // id生成服务
6   test/
7      |-- genglobalid_test.go // 测试文件
8   main.go 
9   go.mod
10   go.sum

  • 测试用例名称一般命名为  Test
     加上待测试的方法名。
  • 测试用的参数有且只有一个,在这里是  t *testing.T

  • 模糊测试(1.18支持)的参数是 testing.F`,基准测试(benchmark)的参数是 `testing.B
    ,TestMain 的参数是  *testing.M
     类型。


代码实例

main.go
代码如下,初始化路由,启动监听服务

1package main
2
3import "exampleUt/routers"
4
5func main() {
6    router := routers.SetupRouter()
7    router.Run(":8088")
8}

router.go
代码如下,路由初始化,创建一个id生成服务

 1package routers
2
3import (
4    "exampleUt/services"
5
6    "github.com/gin-gonic/gin"
7)
8
9func SetupRouter() *gin.Engine {
10    router := gin.Default()
11
12    router.GET("/globalid", services.GlobalIdServiceInstance.GenGlobalId)
13
14    return router
15}

globalIdService.go
代码如下,id生成具体实现方法

 1package services
2
3import (
4    "time"
5
6    "github.com/gin-gonic/gin"
7    "github.com/sony/sonyflake"
8)
9
10type GlobalIdService struct {
11}
12
13var (
14    GlobalIdServiceInstance *GlobalIdService
15)
16
17func init() {
18    GlobalIdServiceInstance = &GlobalIdService{}
19
20    //初始化id生成服务,使用sonyflake算法
21    t, _ := time.Parse("2006-01-02""2019-06-01")
22    settings := sonyflake.Settings{
23        StartTime: t,
24    }
25
26    sf = sonyflake.NewSonyflake(settings)
27    if sf == nil {
28        panic("sonyflake not created")
29    }
30}
31
32var sf *sonyflake.Sonyflake
33
34//生成全局id方法
35func (c *GlobalIdService) GenGlobalId(g *gin.Context) {
36    id, _ := sf.NextID()  //生成id
37    g.JSON(200, sonyflake.Decompose(id)) //返回结果
38}

genglobalid_test.go
测试代码如下

 1package test
2
3import (
4    "bytes"
5    "encoding/json"
6    "exampleUt/routers"
7    "fmt"
8    "io/ioutil"
9    "net/http"
10    "net/http/httptest"
11    "testing"
12)
13
14type GlobalId struct {
15    Id        uint64 `json:"id"`
16    Machineid uint64 `json:"machine-id"`
17    Msb       uint64 `json:"msb"`
18    Sequence  uint64 `json:"sequence"`
19    Time      uint64 `json:"time"`
20}
21
22//测试函数 t *testing.T 作为入参
23func TestGenGlobalId(t *testing.T) {
24    // 初始化请求地址
25    uri := "/globalid"
26    router := routers.SetupRouter()
27
28    // 发起Get请求
29    rsp := PerformRequest(uri, "GET"nil, router)
30    if rsp.StatusCode != 200 {
31        t.Error("请求响应状态错误:" + rsp.Status) //抛出测试错误信息
32    }
33    // 读取响应body
34    body, _ := ioutil.ReadAll(rsp.Body)
35    fmt.Printf("response:%v\n"string(body))
36
37    var globalid GlobalId
38    err := json.Unmarshal(body, &globalid)
39    if err != nil {
40        t.Error("响应数据格式不正确,body:"string(body)) //抛出测试错误信息
41    }
42
43    if globalid.Id == 0 {
44        t.Error("id 生成错误:", globalid.Id)
45    }
46
47}
48
49//通用http请求
50func PerformRequest(path, method string, param []byte, router http.Handler) *http.Response {
51    // 构造post请求,json数据以请求body的形式传递
52    req := httptest.NewRequest(method, path, bytes.NewReader(param))
53
54    // 初始化响应
55    w := httptest.NewRecorder()
56
57    // 调用相应的handler接口
58    router.ServeHTTP(w, req)
59
60    // 提取响应
61    result := w.Result()
62    return result
63}

进入测试代码目录,运行  go test
,该 package
下所有的测试用例都会被执行。

 1$ cd test
2$ go test 
3[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
4
5[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
6 - using env:   export GIN_MODE=release
7 - using code:  gin.SetMode(gin.ReleaseMode)
8
9[GIN-debug] GET    /globalid                 --> exampleUt/services.(*GlobalIdService).GenGlobalId-fm (3 handlers)
10[GIN2022/03/31 - 14:12:33 | 200 |       42.25µs |       192.0.2.1 | GET      "/globalid"
11response:{"id":149921124212277522,"machine-id":274,"msb":0,"sequence":0,"time":8935995353}
12PASS
13ok      exampleUt/test  0.587s

或  go test -v
-v
 参数会显示每个用例的测试结果,另外  -cover
 参数可以查看覆盖率。

 1$ go test -v -cover
2=== RUN   TestGenGlobalId
3[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
4
5[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
6 - using env:   export GIN_MODE=release
7 - using code:  gin.SetMode(gin.ReleaseMode)
8
9[GIN-debug] GET    /globalid                 --> exampleUt/services.(*GlobalIdService).GenGlobalId-fm (3 handlers)
10[GIN2022/03/31 - 15:03:14 | 200 |     115.625µs |       192.0.2.1 | GET      "/globalid"
11response:{"id":149926225693901074,"machine-id":274,"msb":0,"sequence":0,"time":8936299425}
12--- PASS: TestGenGlobalId (0.00s)
13PASS
14coverage: [no statements]
15ok      exampleUt/test  0.656s

如果有多个测试用例,而你只想运行其中的一个用例,例如  TestGenGlobalId
,可以用  -run
 参数指定,该参数支持通配符  *
,和部分正则表达式,例如  ^ 、 $

1$ go test -run TestGenGlobalId -v

小结

标准库提供的测试包,还有很多很多好的工具,比如Mock
,后面继续分享吧。
以测试的角度,推行单元测试是不易的,最佳的方式莫过于开发人员,在一定的指引之后,以实际项目出发进行实践,然后自行总结,有针对性进行内部分享,共同学习进步,才能真正的落地。

最后我们会发现,做好单元测试,是一件事半功倍的事情。


如果您喜欢本期教程欢迎点赞、转发、关注 !


更多详情关注本公众号留言获取。


版权声明:本文来自原创,版权归猿武场作者所有。如需转载,请联系作者并注明出处。


注公众号并回复数字「 1024 」加入猿武场微信社群 

欢迎加入程序员社群,更多技术摘要等你拿走

社群福利:

1. 行业大牛技术手札,知识点汇总

2. 求职/招聘信息内推

4. 人际交往,增强技术宅人际交流;

5. 调节繁杂无趣的闲暇时光;

6. 不定期线上周边线下技术活动沙龙


代 码 / 改 / 变 / 世 / 界
感谢您对猿武场的关注与支持


文章转载自猿武场,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论