七牛云李意扬:如何收集 Go 实时覆盖率丨ECUG Meetup 回顾
七牛云李意扬:如何收集 Go 实时覆盖率丨ECUG Meetup 回顾 https://mp.weixin.qq.com/s/aKI0NO4-WQwK0gxF7RDSnA
七牛云李意扬:如何收集 Go 实时覆盖率丨ECUG Meetup 回顾
接触过 Go 的同学知道,官方没有提供集成测试覆盖率的收集方案。针对集成测试覆盖率的需求,七牛云于 2020 年开源了一款工具 Goc (https://github.com/qiniu/goc) —— Go 集成测试覆盖率收集利器,能很好地解决这个问题。
本文主题「如何收集 Go 实时覆盖率」,根据李意扬(七牛云 视频业务质量保障负责人&测试开发专家)于 2021 年 12 月 4 日举办的「ECUG Meetup 第 3 期 | 工程效率优化与最佳实践专场 · 上海站」上的分享整理而成。 获取「讲师完整版 PPT」,请关注七牛云技术团队公众号并回复关键词「ECUG 第三期」,获取下载链接。其余讲师的分享也将于后续陆续放出,敬请期待。
七牛云技术团队
七牛云技术团队,关注技术实践,热衷技术分享,这里有 Go+、Go、PaaS、云原生、大数据、音视频、实时互动、AIOps 等领域的技术干货。
公众号
以下为分享正文:
大家好,我是七牛云视频业务保障负责人李意扬。 开始之前,我先打一个广告:七牛云是一家提供「云+数据」服务的一体化 PaaS 平台,除了 CDN 和存储业务之外,还有 RTC、直播、机器数据分析等业务。大家如果有这方面的需求,可以尝试用一下我们七牛云的产品。
我今天分享的题目是「如何收集 Go 的实时覆盖率」,我先给出我们七牛云的答案:做一个 Go 版的 JaCoCo。
当前业界没有完善的 Go 集成测试覆盖率解决方案,为此我们开发了一个工具,就是 Goc!今天我会介绍下 Goc 的使用方法,简单分享它的原理,最后再带大家看一下借助 Goc 可以做哪些有趣的事情。
Go 单测覆盖率
Go 官方提供了单测覆盖率收集的方法。举个例子,有两个文件,被测业务代码 kcp.go 和单测代码 kcp-test.go ,我们用 go test 工具运行单测,打开 -cover 选项就可以收集到单测覆盖率,再开启 -coverprofile 选项,就可以将覆盖率报告输出到本地。 如何解读这个覆盖率报告?Go 不是对每一行去统计,而是把整个代码拆分成了一个个语句块。覆盖率报告的每一行开头表示该语句块所在的文件名,接着是语句块的起始行和起始列,然后是语句块的结束行、结束列,倒数第二列为语句块内的总行数,最后一列为该语句块在这次测试中被执行到的次数。结合所有的语句数量和执行次数便可计算出整个代码的覆盖率了。 但测试不仅仅只有单测,测试同学平时接触最多的反而是手工、集测、系统和自动化测试,对于这些测试手段与被测程序分离、处于不同二进制的情况,需要新的覆盖率收集方法。
Goc vs 传统的集测覆盖率收集
目前业界的覆盖率收集方案如下:
1. 首先在原工程中新增单测代码,添加测试用例 TestMainStart,在该用例最后调用被测业务 main。
2. 然后使用 go test -c 选项将测试用例编译成可执行文件 cover.test。func TestMainStart(t *teing.T){
van args[]string
for _, arg := range os.Args{
if !strings.HasPrefix(arg, "-test"){
args = append(args,arg)
}
}
os.Angs = args
main()
}
→ go test -coverpkg="./..." -c -o cover.test
3. 最后用 -cover.run 指定运行我们封装过的测试用例 TestMainStart。
→ ./cover.test -test.run "TestMainStart" -test.coverprofile=cover.out
cover.test 运行起来后,我们就可以进行各种手工、集测、系统和自动化测试了。由于 cover.test 这个二进制本质上是一个 go test 的单测代码,所以当程序退出后会自动输出覆盖率报告,我们也就能得到被测业务的集测覆盖率了。但该方案有很多缺点:
1. 工程中需要新增测试代码,如果工程中有多个 main 函数,就得为每一个 main 函数编写测试用例。2. 改变了程序启动的方式,必须加上 -test.run 和 -test.coverprofile 两个参数。3. 被测程序退出后才能得到覆盖率报告,无法获取实时覆盖率。4. 覆盖率报告只能输出到本地,需要编写脚本集中收集。5. 分布式微服务架构下,需要对每个服务、多份报告合并才能得到整个系统的覆盖率。 让我们看看 Goc 如何解决这些缺点:
1. 如果原工程是用 go build 命令编译的,那么现在只需替换为 goc build 命令编译,即可得到一个支持覆盖率收集的可执行文件。原工程代码无须做任何改动。
2. 使用 Goc 编译出的二进制,不会破坏原程序的启动方式。
3. 被测程序不再需要退出就能收集覆盖率。如下面动图所示。左侧是一个被测的 HTTP 服务。右侧是用 goc 命令获取的实时覆盖率。上文介绍了报告最后一列代表了语句块执行的次数,当我用 curl 命令访问了被测 HTTP 服务后,可以看到部分语句块执行次数从 0 变成了 1,整个测试过程被测业务正常运行。
4. 覆盖率报告统一收集、自动合并。如下动图所示,左边是两个被测的 HTTP 服务,一个监听在 5000 端口,另一个监听在 5001 端口。首先访问其中 5000 端口的服务,可以看到部分语句块执行次数从 0 变到了 1。然后访问 5001 端口的服务,语句块执行次数从 1 变成了 2。Goc 所展示的覆盖率报告不再是单个服务的,而是把被测系统看作了一个整体。另外动图中还展示了 Goc 实时清除/重置覆盖率的能力,测试人员可以结合该能力对某个功能反复测试。
Goc 的原理
接下来我们简单介绍一下 Goc 的原理。 回顾语句覆盖率的定义:
测试时运行被测程序后,程序中被执行到可知性语句的比率。从这个定义出发我们有一个朴素的想法,能不能在每一行运行完后加入一些统计计算的逻辑去统计覆盖率呢? 一些语言正是这样做的,比如 Python。Python 是带解释器的动态语言,标准库提供了方法 sys.settrace 给解释器设置一个回调函数,当解释器每执行一行代码就会调用一次回调函数,传入当前行的行号,最后统计所有执行到的行数,便可得到覆盖率。 C++ 没有解释器,所以只能在运行前往代码中插入技术器。但 C++ 认为每一行统计非常低效,对性能损失很大。所以,它把代码分成了很多的基本块,在基本块之间的跳转处插入一个计数器,等待程序结束之后统计这些计数器的结果,便可得到全局的覆盖率。
Java 的做法和 C++ 非常类似,但没有在汇编层做插桩,而是在字节码处插桩。Java 虚拟机提供了动态改变字节码的能力,Jacoco 在跳转处插入探针 Probe 做覆盖率收集。
我们回到 Go,看一下 go test 单元测试怎么收集覆盖率的。Go 也是把代码分成了很多语句块。比如说这段代码就分成了三块:
但和 C++、Java 在块之间跳转处插桩不同, Go 在语句块中插入了计数器:
上述计数器的定义如下:
Count 这个字段代表计数器,Pos 就是代表了语句块起始行、起始列、结束行和结束列,NumStmt 代表了语句块内的语句数。 这个方案是源码级的插桩,所以势必会破坏源码。Goc 首先会把整个工程拷贝到临时目录。var GoCover = struct {
Count [3]uint32
Pos [3 * 3]uint32
NumStmt [3]uint16
} {
Pos: [3 * 3]uint32{
5, 7, 0xc000d, // [0]
7, 9, 0x3000c, // [1]
9, 11, 0x30008, // [2]
}
NumStmt: [3]uint16{
2, // 0
1, // 1
1, // 2
},
}
接着使用标准库 ast/parser 解析出语法树,在每个语句块中插入计数器写入临时目录,最后调用原生的 go build/install/run 命令去编译插过桩的代码。 除了计数器,我们还为每个 main 包注入了一个 HTTP API。调用暴露的 HTTP 接口会计算聚合每个计数器,并返回服务当前的覆盖率。插桩服务启动后,首先会访问 goc server 这个注册中心,上报自己 ip 地址和 HTTP API 端口。goc server 注册中心存储了每个被测服务的信息。
客户端向 goc server 注册中心获取覆盖率报告时,中心会拉取所有服务的覆盖率并合并成单份报告,然后返回给客户端。 在实际公测中,有用户反馈在云原生场景下集成 goc 非常不便,比如 docker/k8s 启动服务必须加 -p 参数,将外部的端口映射到内部的端口,才能让注册中心 goc server 访问到被插桩的服务。这个问题的本质是因为 goc server 注册中心和被插桩服务分属不同网络,它们通过 NAT 实现网络转换。 Goc 在 v2 版本给出了改进方案,v2 将所有内部通信构建在了 websocket 长连接之上,整个覆盖率收集系统只需保证 goc server 能被访问即可,对测试系统的部署侵入降到了最低,更适合云原生业务的测试。
Goc 扩展
由于 Goc 输出的覆盖率报告和 Go 官方的单测覆盖率报告完全一致。一些已有的分析单测覆盖率的系统可以直接用来分析 Goc 收集的集测覆盖率,比如说 SonarQube、Codecov、Covralls。 借助 Goc 覆盖率实时收集的能力还可以做精准测试。这里有两个方案:
1. Goc v1 版中,cmd: goc profile 轮询获取全量覆盖率2. Goc v2 版中,cmd: goc watch 实时推送增量覆盖率,下图展示的是实时推送的增量覆盖率
最后再展示我们实现的一个 VS Code 插件 demo,动图中随着访问不同的 HTTP API 接口,代码相应的位置被高亮了起来,研发和测试同学可以基于该插件实现精准的白盒测试。
上述 VS Code 插件使用的公开接口如下表所示。
Goc v1 接口 | 功能 |
GET /v1/cover/profile | 获取覆盖率 |
POST /v1/cover/clear | 重置覆盖率 |
GET /v1/cover/list | 获取服务列表(仅服务名、ip) |
POST /v1/cover/remove | 删除服务 |
Goc v2 接口 | 功能 |
GET /v2/cover/profile | 获取覆盖率 |
DELETE /v2/cover/profile | 重置覆盖率 |
GET /v2/agents | 获取服务列表(服务名、ip、服务在线状态) |
DELETE /v2/agents | 删除服务 |
ws://xxx/v2/cover/ws/watch | 实时推送覆盖率 |