测试基础 什么是代码覆盖率

爱学习的饲养员 · 2024年09月05日 · 最后由 GeorgeZhuang 回复于 2024年09月09日 · 5018 次阅读

不知道写过接口自动化 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 文件:

// 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 文件:

// 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: %v\n", sig)
   case val := <-exitChan:
      fmt.Printf("exit as received http request: %v\n", val)
   }
}

1.2 执行 go test 命令

进入 main_test.go 所在目录,一般在代码根目录,执行 go test 命令。让 go test 命令 启动 Web 服务进行测试,产出覆盖率文件。

go test -coverprofile=cov.out -coverpkg ./... &
# 参数介绍
# -coverprofile 指定产出的覆盖率文件名称
# -coverpkg ./... 指包含该路径下所有子包的覆盖率结果不加此参数可能会导致覆盖率结果中只有main文件
# & 让服务进程后台运行避免启动后马上退出必须加!!!

执行完命令后,可以看到服务正常启动的日志(确保 Web 服务已经正常启动)

1.3 执行测试用例

Web 服务启动后,就可以开始执行你的测试用例了,例如:

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 生成代码覆盖率文件

用例执行结束,执行以下命令,发送指令停止服务,否则无法正常生成覆盖率文件。

curl 127.0.0.1:9999  

此时,在代码根目录将会生成 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

1.5 查看覆盖率报告

为了方便查看和浏览,可将 out 文件转换为 html 报告进行查看,执行如下命令:

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 来插桩。

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: %v\n", sig)
      case val := <-exitChan:
         fmt.Printf("exit as received http request: %v\n", val)
      }
   }
}

该文件定义了一个 systemTest 标志,并包含一个调用 main 函数的测试用例。
运行测试二进制文件开始执行测试。在我们的例子中,这意味着调用 TestExternal,因为这是唯一的测试。运行 TestExternal 意味着调用 main 函数,它将像普通二进制文件那样启动应用程序。这也就意味着运行测试产生的二进制文件与运行普通二进制文件相同,只是运行测试产生的二进制文件将会跟踪覆盖率执行,也就是我们常说的打桩。
为了防止在运行单元测试时运行此测试,添加了命令行标志 systemTest。如果未设置,则不会调用 main 函数。而要运行系统测试,必须在执行测试二进制文件期间通过附加-systemTest 来设置标志。

2.2 生成插桩后的覆盖率二进制文件

在代码根目录执行以下命令:

go test -c -covermode=count -coverpkg ./...

执行完成后将生成一个 *.test 文件

2.3 执行二进制文件

要查看二进制文件是否按照预期生成,可以手动执行它,查看服务是否正常启动。

./hello-go.test -systemTest -test.coverprofile cov.out

2.4 执行测试用例

在服务启动后,如同方法一类似,执行你的用例,例如:

curl 127.0.0.1:8001        # 用例1
curl 127.0.0.1:8001/api1   # 用例2
curl 127.0.0.1:8001/api2   # 用例3

用例执行完毕后,执行以下命令停止服务:

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 报告进行查看,执行如下命令:

go tool cover -html cov.out -o index.html

结束语

代码覆盖率不是灵丹妙药,它只是告诉我们有哪些代码没有被测试用例 “执行到” 而已,高百分比的代码覆盖率不等于高质量的有效测试。
高代码覆盖率不足以衡量有效测试,具有高代码覆盖率并不能充分表明我们的代码已经过充分测试。相反,代码覆盖率更准确地给出了代码未被测试程度的度量。这意味着,如果我们的代码覆盖率指标较低,那么我们可以确定代码的重要部分没有经过测试,然而反过来不一定正确。作为测试同学,我们还是要进行代码走查等测试活动,而不是一味的追求高覆盖率。

共收到 3 条回复 时间 点赞

请问下楼主,1.3 执行测试用例 这里的用例是指啥呢 直接访问接口也算吗?

代码覆盖率不仅可以作为测试用例的补充,也可以在项目流程上作为准出的卡点,提成整体质量

3楼 已删除

我在网站首页右边看到一家做覆盖率产品的公司广告,我下载并使用了下,挺好用的。

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册