go单元测试

契机

单元测试其实一直是我想做但是没有做的事,写php这么久其实也没怎么写过单测。我给自己找的借口是。

  1. 项目开发工期紧任务重
  2. 项目代码混乱现在已经没法写出单测代码

但是经历了以下的一些事我决定养成良好的写单测的习惯。

  1. 毛剑的 Open Go训练营反复强调了单测
  2. 在给耗子的megaease提pr的时候要求带上单测
  3. 看王峥的《设计模式之美》的时候又反复强调了测试性和重构后的单测

通过上面几件事我认识到想写出不被诟病的好代码除了有良好的 英文命名,优秀的工业设计外还需要覆盖率尽可能高的单元测试

go单元测试

go test 基础

go test 主要功能是提供 单元测试基准测试。我主要记录了单元测试

1
2
3
4
5
6
-bench regexp 执行相应的 benchmarks,例如 -bench=.;
-cover 开启测试覆盖率;
-run regexp 只运行 regexp 匹配的函数,例如 -run=Array 那么就执行包含有 Array 开头的函数;
-v 显示测试的详细命令。
-covermode=count 覆盖率
-coverprofile=count.out 覆盖率详情输出到文件

代码规范

  1. *_test.go 结尾
  2. 函数 Test* 开头

事例

代码

我写了一个用chromedp操作chrome的封装函数如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
package browser

import (
"context"
"errors"
"github.com/chromedp/cdproto/cdp"
"github.com/chromedp/cdproto/network"
"github.com/chromedp/cdproto/runtime"
"github.com/chromedp/chromedp"
"log"
"time"
)

type Browser struct {
ctx context.Context
Cancelfunc context.CancelFunc
}

func (b *Browser) NewBrowser() {
opts := append(chromedp.DefaultExecAllocatorOptions[:],
chromedp.Flag("headless", true), // 开启窗口模式
chromedp.Flag("window-size", "1920,1080"),
)
b.ctx, b.Cancelfunc = chromedp.NewExecAllocator(context.Background(), opts...)
b.ctx, _ = chromedp.NewContext(b.ctx, chromedp.WithLogf(log.Printf))
}

func (b *Browser) RunGroup (url string , taks ...chromedp.Tasks) error{

actions := chromedp.Tasks{}
actions = append(actions, chromedp.Navigate(url))
for _,v := range taks {
actions = append(actions,v)
}

if err := chromedp.Run(
b.ctx,
actions,
); err != nil {
return err
}
return nil
}

func (b *Browser) Js(js string) chromedp.Tasks {
return chromedp.Tasks{
chromedp.ActionFunc(func(ctx context.Context) error {
_, exp, err := runtime.Evaluate(js).Do(ctx)
if err != nil {
return err
}
if exp != nil {
return exp
}
return nil
}),
}
}

func (b *Browser) SetCookie(host string, cookies ...string) chromedp.Tasks {
if len(cookies)%2 != 0 {
return chromedp.Tasks{
chromedp.ActionFunc(func(ctx context.Context) error {
return errors.New("length of cookies must be divisible by 2")
}),
}
}

return chromedp.Tasks{
chromedp.ActionFunc(func(ctx context.Context) error {
// create cookie expiration
expr := cdp.TimeSinceEpoch(time.Now().Add(180 * 24 * time.Hour))
// add cookies to chrome
for i := 0; i < len(cookies); i += 2 {
err := network.SetCookie(cookies[i], cookies[i+1]).
WithExpires(&expr).
WithDomain(host).
WithHTTPOnly(true).
Do(ctx)
if err != nil {
return err
}
}
return nil
}),
}
}

func (b *Browser) Screenshot(buff *[]byte, quality int) chromedp.Tasks{
return chromedp.Tasks{
chromedp.FullScreenshot(buff,quality),
}
}

然后写了一个测试用例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package browser

import (
"github.com/stretchr/testify/assert"
"testing"
)


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

b := Browser{}

b.NewBrowser()
defer b.Cancelfunc()

url := "https://www.baidu.com"
e := b.RunGroup(url)
assert.Nil(e)
e = b.RunGroup("error url")
assert.NotNil(e)


e = b.RunGroup(url, b.Js("console.log(123)"))
assert.Nil(e)
e = b.RunGroup(url, b.Js("-----"))
assert.NotNil(e)

e = b.RunGroup(url, b.SetCookie(url,"zjj","123"))
assert.Nil(e)
e = b.RunGroup(url, b.SetCookie(url,"abc","123","111"))
assert.NotNil(e)

var buff []byte
e = b.RunGroup(url, b.Screenshot(&buff ,100))
assert.Nil(e)

if len(buff) <= 100 {
assert.Fail("image size err")
}

}

查看单测是否通过

覆盖率

查询覆盖率并且输出到文件做后续分析

1
2
go test chromedp*.go --covermode=count
go test chromedp*.go --coverprofile=count.out

1
go tool cover -func ./count.out

1
go tool cover -html ./count.out

相关类库

我个人认为这些库没必要每个都去学习而是用到再看,我了解一些常用的类库比如 testify和monkey考虑到篇幅有限而且它们文档已经很完善了,就没在这边做扩展。

名称 地址 作用
testify gihub assert高频使用,主要提供*testing.T的封装操作
mockery github Golang 的模拟代码自动生成器
httptest 文档 提供http的测试
go-sqlmock github Sql mock driver
miniredis github redis mock
monkey github 汇编语言重写可执行文件的打桩工具

基于阿里云发布案例

ps:除了用阿里用的codeup外其实GitHub支持也很完善,因为我并不打算把代码开源并且我一直用的云效的发布流程部署代码到服务器上,因此此我托管到了阿里code。GitHub 其实有 看见耗子哥用 https://docs.codecov.com 来做单测报告

新建仓库

创建忽略文件,关联远程分支,上传本地代码

新建流水线

在流水线中新建go的单元测试

测试命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
apt-get update
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
apt-get install -y dpkg
dpkg -i ./google-chrome-stable_current_amd64.deb
apt-get install -f -y
dpkg -i ./google-chrome-stable_current_amd64.deb
# 默认使用goproxy.cn用户可手动调整
export GOPROXY=https://goproxy.cn
# 默认的单元测试命令
# 输出测试报告目录到当前工作目录,可自动上传并展示
mkdir -p golang-report
# 未使用Go Mod的用户需要打开一下注释
# export GOFLAGS=-mod=vendor
go test -v -json -cover -coverprofile cover.out ./... > golang-report/report.jsonl
go tool cover -html=cover.out -o golang-report/index.html

我们可以自己设置红线信息然后在后台看见测试报告,如果不符合红线设置代码是无法进入下一个阶段的

接下来的事就是根据自己的需要了,是根据二进制部署还是打包进docker

一些思考

其实现在单元测试不管是PHP还是GO从语言本身和环境生态上都支持得很到位了,我们进行单元测试有几个好处

  • 有效的避免功能迭代或变更时的BUG
  • 控制代码质量(自己都没法写出单测的代码是有问题的
  • 项目不断重构是个过程,单测可以在重构中起到比较好的作用
  • 考虑到更多的边界情况

实践

更新 2022年10月06日01:36:31

看了下这篇日志大概写了1个月了,这是一个月来我对go单元测试的完整流程实践。

GitHub Actions + codecov
https://github.com/zhangjunjie6b/mysql2elasticsearch

产考

http://c.biancheng.net/view/124.html

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!

请我喝杯咖啡吧~