前言
大家好,我是洋子。不知道写过接口自动化case的朋友们,有没有思考过一个问题。假如我写了很多接口自动化case,已经把被测系统的所有接口都覆盖到,那这是不是就说明我的自动化case已经全部写完了?是不是就说明我的自动化测试已经做得非常完备了?
答案是否定的
因为我们缺少数据
来衡量自动化case的完备程度,那该怎么解决呢
业界一般是通过代码覆盖率
来输出自动化case的覆盖数据,衡量接口自动化测试的完备程度,来指导后续要增加、完善case的方向。另一方面,它还可以反映服务端功能测试的全面性,用来评估服务端手工测试是否全面
除此以外,代码覆盖率还可以应用于单元测试
,可以拿到经过执行单元测试用例后的覆盖率数据
覆盖率定义
作为一个测试人员,保证产品的软件质量是其工作首要目标,为了这个目标,测试人员常常会通过很多手段或工具来加以保证,覆盖率就是其中比较重要的环节,我们通常会将测试覆盖率分为两个部分,即需求覆盖率
和代码覆盖率
需求覆盖:指的是测试人员对需求的了解程度,根据需求的可测试性来拆分成各个子需求点,来编写相应的测试用例,最终建立一个需求和用例的映射关系,以用例的测试结果来验证需求的实现,可以理解为黑盒覆盖
代码覆盖:为了更加全面的覆盖,我们可能还需要理解被测程序的逻辑,需要考虑到每个函数的输入与输出,逻辑分支代码的执行情况,这个时候我们的测试执行情况就以代码覆盖率来衡量,可以理解为白盒覆盖
。例如,如果源代码具有一个简单的if...else循环,则如果测试代码可以覆盖这两种情况(即if&else),则代码覆盖率将为100%
代码覆盖率,是一种通过计算测试过程中 被执行的源代码
占 全部源代码
的比例,进而间接度量软件质量的方法。它在保证测试质量的时候潜在保证实际产品的质量,可以基于此在程序中寻找没有被测试用例测试过的地方,进一步创建新的测试用例来增加覆盖率。常见的编程语言,如Java,C ,Python,JavaScript,PHP和Go等,都有相应的代码覆盖率统计工具
语言种类 | 覆盖率统计工具 |
---|---|
Java | cobertura、jacoco |
C | ccover、Lcov |
Python | Coverage.py |
JavaScript | istanbul |
PHP | xdebug、phpunit、PATest、xcache、php-code-coverage |
Go | gocov、go test |
为什么要测量代码覆盖率
我们在做单元测试或者接口自动化测试时,你是否知道你的单元测试甚至是你的功能测试实际测试代码的效果?是否还需要更多的测试?这些是代码覆盖率可以试图回答的问题。总之,出于以下原因我们需要测量代码覆盖率:
- 了解我们的测试用例对源代码的测试效果
- 了解我们是否进行了足够的测试
- 在软件的整个生命周期内保持测试质量
注:代码覆盖率不是灵丹妙药,覆盖率测量不能替代良好的代码审查和优秀的编程实践
Go覆盖率统计
Go语言是现在互联网大厂很常用的语言,下面就结合go test
命令行工具,来讲解 如何统计单元测试或者接口自动化测试代码覆盖率
对于go test 提供了两种统计覆盖率的方式,一种是直接使用go test 命令行,另外一种则是执行插桩后的二进制文件
温馨提示:阅读以下内容,需要掌握Go语言的基础语法
方法一:直接运行go test 命令统计覆盖率
1.1 创建main_test.go 文件
创建main_test.go 文件或者与你的main函数所在的文件名同名的test文件,假如我们有以下main.go文件:
代码语言:javascript复制// main.go文件
package main
import (
"github.com/labstack/echo"
"hello-go/api"
)
func main() {
e := echo.New()
e.GET("/", api.HelloWorld)
e.GET("/api1", api.Api1)
e.GET("/api2", api.Api2)
e.Logger.Fatal(e.Start(":8001"))
}
根据以上main.go文件,创建以下main_test.go文件
代码语言:javascript复制// main_test.go文件
package main
import (
"fmt"
"net/http"
"os"
"os/signal"
"testing"
)
var exitChan chan int
func testHandler(w http.ResponseWriter, req *http.Request) {
exitChan <- 666
}
func startServer() {
go main() // 调用main.go文件中的main方法,启动服务
}
func TestExternal(t *testing.T) {
// start server need be tested in separate go thread
go startServer()
// go test starts a dummy http server, which is used to
// end the current go test gracefully when it's accessed.
http.HandleFunc("/", testHandler)
go http.ListenAndServe(":9999", nil)
// go test只有在服务进程正确退出的情况下才会生成覆盖率文件,因此在这里注册9999端口,来监听停止服务的指令,
// 当完成测试后,向9999端口触发请求,服务停止,生成覆盖率文件
exitChan = make(chan int)
sigChann := make(chan os.Signal)
signal.Notify(sigChann, os.Interrupt)
select {
case sig := <-sigChann:
fmt.Printf("exit as received signal: %vn", sig)
case val := <-exitChan:
fmt.Printf("exit as received http request: %vn", val)
}
}
1.2 执行go test 命令
进入main_test.go所在目录,一般在代码根目录,执行go test命令。让go test 命令 启动Web服务进行测试,产出覆盖率文件
代码语言:javascript复制go test -coverprofile=cov.out -coverpkg ./... &
# 参数介绍
# -coverprofile 指定产出的覆盖率文件名称
# -coverpkg ./... 指包含该路径下所有子包的覆盖率结果,不加此参数可能会导致覆盖率结果中只有main文件
# & 让服务进程后台运行,避免启动后马上退出,必须加!!!
执行完命令后,可以看到服务正常启动的日志(确保Web服务已经正常启动)
1.3 执行测试用例
Web服务启动后,就可以开始执行你的测试用例了,例如:
代码语言:javascript复制curl 127.0.0.1:8001 # 用例1
curl 127.0.0.1:8001/api1 # 用例2
curl 127.0.0.1:8001/api2 # 用例3
1.4 生成代码覆盖率文件
用例执行结束,执行以下命令,发送指令停止服务,否则无法正常生成覆盖率文件
代码语言:javascript复制curl 127.0.0.1:9999
此时,在代码根目录将会生成cov.out覆盖率文件,cat cov.out 内容如下所示:
代码语言:javascript复制mode: set
hello-go/api/apis.go:9.39,11.2 1 1
hello-go/api/apis.go:13.34,15.2 1 1
hello-go/api/apis.go:17.34,19.2 1 0
hello-go/main.go:8.13,14.2 5 1
1.5 查看覆盖率报告
为了方便查看和浏览,可将out文件转换为html报告进行查看,执行如下命令:
代码语言:javascript复制go tool cover -html cov.out -o index.html
方法二:编译、执行插桩二进制文件统计覆盖率
除了直接运行go test
命令,我们还可以通过运行插桩二进制文件来统计覆盖率
2.0 生成覆盖率二进制文件(插桩产物)原理介绍
要运行系统进行测试,需要应用程序的编译二进制文件。然后,在具有不同配置的不同环境中执行此二进制文件。Golang提供了一种独特的方法来生成覆盖率二进制文件,而不是go build
生成的默认二进制文件
生成的代码覆盖率二进制文件在每一行代码后写入一个唯一的计数器,并检查在执行二进制文件后调用此计数器的次数
更多的技术细节可以在go-cover文档(https://go.dev/blog/cover)中找到
当执行go test
时,覆盖率二进制文件会自动生成并在之后处理。Golang允许使用以下命令生成此覆盖率二进制文件
go test -c -covermode=count -coverpkg ./...
# 参数介绍
# -c 标志用于生成测试二进制文件
# -covermode=count 确保生成的二进制文件中包含覆盖率计数
# -o 可以指定生成的二进制文件的名称,如不设该参数,生成的文件将被自动命名为packagename.test
# -coverpkg ./... 在命令末尾,确保为同一路径下的所有子包生成覆盖率二进制文件,但不为导入的包生成覆盖率二进制文件。如果您只想覆盖特定的包,可以在这里用逗号分隔它们
更多的参数信息可以执行go test -help
来查看
2.1 创建main_test文件
现在我们知道了如何生成二进制文件,我们必须确保二进制文件将按预期执行。您的代码需要满足以下要求,才能按照预期生成二进制
package中至少有一个
*_test.go
文件,否则不会生成二进制文件。我建议创建main_test.go 文件,或者与你的main函数所在的文件名同名的test文件
与方法一类似,需要创建一个main_test.go文件让go test来插桩
代码语言:javascript复制package main
import (
"flag"
"fmt"
"net/http"
"os"
"os/signal"
"testing"
)
var exitChan chan int
func testHandler(w http.ResponseWriter, req *http.Request) {
exitChan <- 666
}
var systemTest *bool
func init() {
// systemTest 参数,区分运行时是否执行系统测试
systemTest = flag.Bool("systemTest", false, "Set to true when running system tests")
}
func TestExternal(t *testing.T) {
if *systemTest {
// start server need be tested in separate go thread
// 调用main.go文件中的main方法,启动服务
go main()
// go test starts a dummy http server, which is used to
// end the current go test gracefully when it's accessed.
http.HandleFunc("/", testHandler)
go http.ListenAndServe(":9999", nil)
// go test只有在服务进程正确退出的情况下才会生成覆盖率文件,因此在这里注册9999端口,来监听停止服务的指令,
// 当完成测试后,向9999端口触发请求,服务停止,生成覆盖率文件
exitChan = make(chan int)
sigChann := make(chan os.Signal)
signal.Notify(sigChann, os.Interrupt)
select {
case sig := <-sigChann:
fmt.Printf("exit as received signal: %vn", sig)
case val := <-exitChan:
fmt.Printf("exit as received http request: %vn", val)
}
}
}
该文件定义了一个systemTest标志,并包含一个调用main函数的测试用例
运行测试二进制文件开始执行测试。在我们的例子中,这意味着调用TestExternal,因为这是唯一的测试。运行TestExternal 意味着调用main函数,它将像普通二进制文件那样启动应用程序。这也就意味着运行测试产生的二进制文件与运行普通二进制文件相同,只是运行测试产生的二进制文件将会跟踪覆盖率执行,也就是我们常说的打桩
为了防止在运行单元测试时运行此测试,添加了命令行标志systemTest。如果未设置,则不会调用main函数。而要运行系统测试,必须在执行测试二进制文件期间通过附加-systemTest来设置标志
2.2 生成插桩后的覆盖率二进制文件
在代码根目录执行以下命令:
代码语言:javascript复制go test -c -covermode=count -coverpkg ./...
执行完成后将生成一个*.test
文件
2.3 执行二进制文件
要查看二进制文件是否按照预期生成,可以手动执行它,查看服务是否正常启动
代码语言:javascript复制./hello-go.test -systemTest -test.coverprofile cov.out
2.4 执行测试用例
在服务启动后,如同方法一类似,执行你的用例,例如:
代码语言:javascript复制curl 127.0.0.1:8001 # 用例1
curl 127.0.0.1:8001/api1 # 用例2
curl 127.0.0.1:8001/api2 # 用例3
用例执行完毕后,执行以下命令停止服务
代码语言:javascript复制curl 127.0.0.1:9999 # 用例执行结束,发送指令停止服务
# 也可以用ctrl c结束服务,但使用ctrl c结束服务需要在编译时【将main函数中的os.Exit()更改为return】
# 如果用ctrl c结束服务,那就不需要注册9999端口了,可根据业务线需求自行调整
# 但通过注册9999端口去停服是最为保险有效的方式
2.5 生成覆盖率文件
服务停止后,将会生成cov.out
覆盖率文件。此时,在代码根目录将会生成cov.out
,cat cov.out
内容如下所示:
mode: set
hello-go/api/apis.go:9.39,11.2 1 1
hello-go/api/apis.go:13.34,15.2 1 1
hello-go/api/apis.go:17.34,19.2 1 0
hello-go/main.go:8.13,14.2 5 1
2.6 查看覆盖率报告
为了方便查看和浏览,可将out文件转换为html报告进行查看,执行如下命令
代码语言:javascript复制go tool cover -html cov.out -o index.html
结束语
代码覆盖率不是灵丹妙药,它只是告诉我们有哪些代码没有被测试用例“执行到”而已,高百分比的代码覆盖率不等于高质量的有效测试
高代码覆盖率不足以衡量有效测试,具有高代码覆盖率并不能充分表明我们的代码已经过充分测试。相反,代码覆盖率更准确地给出了代码未被测试程度
的度量。这意味着,如果我们的代码覆盖率指标较低,那么我们可以确定代码的重要部分没有经过测试,然而反过来不一定正确。作为测试同学,我们还是要进行代码走查等测试活动,而不是一味的追求高覆盖率
另外本文还介绍了两种Go语言统计覆盖率的方法。方法一适用本地调试,而方法二执行插桩文件便于和持续集成(CI)流水线结合