微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

PouchContainer 集成测试覆盖率统计

PouchContainer 集成测试覆盖率统计

作者| 阿里云智能事业群高级测试开发工程师 刘璐

PouchContainer 是阿里巴巴开源的富容器技术,已于 2018 年 9 月正式发布 GA 版本,已经完全达到生产级别。PouchContainer 一直非常重视项目质量,项目的开发者需在提交 PR 时提供与之对应的单测与集成测试代码。这种要求,一方面保证回归质量,同时也减少代码 review 成本,提高合作效率。(更多参考:PouchContainer 开源版本及内部版本一致性实践)

最初,PouchContainer 结合 TravisCI 与 Codecov 工具,为每次 PR 提交运行测试并展示单元测试覆盖率。对于一些添加集成测试的 PR,集成测试的增减所带来的测试覆盖率变化并没有纳入到测试覆盖率的统计中。

集成测试覆盖率的缺失,使得开发者缺少对项目测试覆盖率的更完整认知。为了更全面的展示 PouchContainer 的测试覆盖率,现在 PouchContainer 已经加入了集成测试覆盖率的统计功能本文主要介绍集成测试覆盖率统计在 PouchContainer 中的实现。

Go 测试覆盖率

在介绍集成测试覆盖率统计实现之前,我们需要了解 Golang 的覆盖率统计的原理。Golang 的覆盖率统计,是通过在编译之前重写包的源代码,加入统计信息,然后编译、运行、收集测试覆盖率。有关 Go 测试覆盖率的原理可参考 The cover story (https://blog.golang.org/cover),接下来的内容,主要参考上述文章,并具体列出执行过程。

首先,给出一个待测 Size() 函数,它有多个 switch 分支,代码如下:

package size
func Size(a int) string {
  switch {
  case a < 0:
    return "negative"
  case a == 0:
    return "zero"
  case a < 10:
    return "small"
  }
  return "enormous"
}

对应的测试代码如下:

$ cat size_test.go
package size

import (
    "testing"
    "fmt"
)

type Test struct {
    in  int
    out string
}

var tests = []Test{
    {-1, "negative"},
    {5, "small"},
}

func TestSize(t *testing.T) {
    fmt.Println("a")
    for i, test := range tests {
        size := Size(test.in)
        if size != test.out {
            t.Errorf("#%d: Size(%d)=%s; want %s", i, test.in, size, test.out)
        }
    }
}


执行 go test -x -cover -coverprofile=./size.out 命令,运行测试并统计测试覆盖率。其中,-x 参数打印上述命令的执行过程(需注意:打印的执行步骤信息不完整,如果手动执行输出的步骤,则会运行失败,这是因为 go test 的一些执行步骤并没有打印信息),-cover 参数开启测试覆盖率统计功能-coverprofile 参数指定存储测试覆盖率文件,运行结果如下:


$ go test -x -cover -coverprofile=./size.out
WORK=/var/folders/d2/0gxc6wf16hb6t8ng0w00czpm0000gn/T/go-build982568783
mkdir -p $WORK/test/_test/
mkdir -p $WORK/test/_test/_obj_test/
cd $WORK/test/_test/_obj_test/
/usr/local/go/pkg/tool/darwin_amd64/cover -mode set -var GoCover_0 -o .size.go /Users/letty/work/code/go/src/test/size.go
cd /Users/letty/work/code/go/src/test
/usr/local/go/pkg/tool/darwin_amd64/compile -o $WORK/test/_test/test.a -trimpath $WORK -p test -complete -buildid 6033df309978241f19d83a0e6bad252ee3ba376e -D _/Users/letty/work/code/go/src/test -I $WORK -pack $WORK/test/_test/_obj_test/size.go ./size_test.go
cd $WORK/test/_test
/usr/local/go/pkg/tool/darwin_amd64/compile -o ./main.a -trimpath $WORK -p main -complete -D "" -I . -I $WORK -pack ./_testmain.go
cd .
/usr/local/go/pkg/tool/darwin_amd64/link -o $WORK/test/_test/test.test -L $WORK/test/_test -L $WORK -w -extld=clang -buildmode=exe $WORK/test/_test/main.a
$WORK/test/_test/test.test -test.coverprofile=./size.out -test.outputdir /Users/letty/work/code/go/src/test
a
PASS
coverage: 60.0% of statements
ok      test    0.006s

从上述输出的倒数第二行可知,测试覆盖率为 60%。分析 go test 的执行步骤,第五行调用 /usr/local/go/pkg/tool/darwin_amd64/cover 工具,这个工具重写待测源码,在代码中加入计数点,用以统计测试覆盖率。第 8-13 行编译待测文件_testmain.go 文件(这个文件go test 工具生成的,具体实现细节可以参见https://github.com/golang/go/blob/3f150934e274f9ce167e1ed565fb3e60b8ea8223/src/cmd/go/internal/test/test.go#L1887),生成 test.test 测试执行文件。第 13 行,执行 test.test 测试文件,传入测试相关参数,即可运行测试。

查看 cover 命令的帮助信息,再次执行 cover 命令,可以查看被重写后的测试代码

$ cat .size.go
package size

func Size(a int) string {
    GoCover_0.Count[0] = 1
    switch {
    case a < 0:
        GoCover_0.Count[2] = 1
        return "negative"
    case a == 0:
        GoCover_0.Count[3] = 1
        return "zero"
    case a < 10:
        GoCover_0.Count[4] = 1
        return "small"
    }
    GoCover_0.Count[1] = 1
    return "enormous"
}

var GoCover_0 = struct {
    Count     [5]uint32
    Pos       [3 * 5]uint32
    NumStmt   [5]uint16
} {
    Pos: [3 * 5]uint32{
        3, 4, 0x9001a, // [0]
        12, 12, 0x130002, // [1]
        5, 6, 0x14000d, // [2]
        7, 8, 0x10000e, // [3]
        9, 10, 0x11000e, // [4]
    },
    NumStmt: [5]uint16{
        1, // 0
        1, // 1
        1, // 2
        1, // 3
        1, // 4
    },
}

查看 go test 运行测试后的覆盖率统计文件,信息如下:

$ cat size.out
mode: set
test/size.go:3.26,4.9 1 1
test/size.go:12.2,12.19 1 0
test/size.go:5.13,6.20 1 1
test/size.go:7.14,8.16 1 0
test/size.go:9.14,10.17 1 1

文件的第一行标识覆盖率统计模式为 setgo test 提供 set、count、atomic 三种模式:

  • set 模式仅统计语句是否运行;

  • count 模式统计语句运行的次数

  • atomic 模式与 count 类似,统计语句运行次数,适用于多线程测试。

第二行开始的格式为:name.go:line.column,line.column numberOfStatements count,即文件名、代码的起始位置、语句的行数以及被运行的次数。本次示例代码中,待统计的语句共 5 行,统计模式为 set,共有 3 个 count 被置为 1(读者可以将 covermode 设置为 count,观察 count 输出有何变化),所以最终的测试覆盖率结果为 60%。

PouchContainer 测试覆盖率

PouchContainer 集成 CodeCov 工具,每次运行 TravisCI 会将测试覆盖率文件上传至 CodeCov 网站,完成覆盖率的可视化展示与持续追踪。

TravisCI 与 CodeCov 可以很容易的集成,只需在测试路径下生成一个 coverage.txt 名字的覆盖率统计文件,并在 .tarvis.yml 文件调用 CodeCov 的脚本,即可上传覆盖率统计文件,具体命令可以参考 Makefile 中 TEST_FLAGS= make build-integration-test 里面的实现,感兴趣的同学也可以直接查看 CodeCov 脚本,了解其实现细节。

接下来,我们从单测和集成测试覆盖率统计两方面展开,详细阐述 PouchContainer 的实现细节。

单测覆盖率统计

PouchContianer 收集单测覆盖率相对简单,只需要执行 make unit-test 命令,即可实现覆盖率统计收集。单测覆盖率统计的实现可以可以参考 Makefile。需要注意的是,覆盖率统计时需要排除一些无关 package,例如 vendor 目录、types 目录等,否则会影响测试覆盖率的准确性。

集成测试覆盖率统计

PouchContainer 集成测试,是通过启动 pouch daemon,然后执行 pouch 命令行或者直接发送 API 请求,实现对 daemon API 和命令行的测试。正常情况下,待测试 pouch daemon 是通过 go build编译,源码中没有插入计数器,无法统计测试覆盖率。

实现统计 pouch daemon 的测试覆盖率的 PR 参见https://github.com/alibaba/pouch/pull/1338),这个 PR(由于代码的不断迭代,最新的代码位置已改变,请读者参照本文所对应的 commit 代码)中,我们做了如下工作:

  1. 根目录下新增 main_test.go 测试文件

  2. hack/build 脚本中,新增 testserver 函数用于编译 main package,生成可执行测试文件

  3. hack/make.sh 脚本中,后台启动步骤 2 生成的测试文件,并运行 API 和命令行测试

  4. 测试结束后,给测试进程发送信号,并收集测试覆盖率

接下来将详细讲述实现细节,首先,新增 main_test.go 测试文件,并在文件中定义一个测试函数 TestMain代码如下:

package main

import (
    "os"
    "os/signal"
    "strings"
    "syscall"
    "testing"
)

func TestMain(t *testing.T) {
    var (
        args []string
    )

    for _, arg := range os.Args {
        switch {
        case strings.HasPrefix(arg, "DEVEL"):
        case strings.HasPrefix(arg, "-test"):
        default:
            args = append(args, arg)
        }
    }

    waitCh := make(chan int, 1)

    os.Args = args
    go func() {
        main()
        close(waitCh)
    }()

    signalCh := make(chan os.Signal, 1)
    signal.Notify(signalCh, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGHUP)
    select {
    case <-signalCh:
        return
    case <-waitCh:
        return
    }
}

通过添加 main_test.go 文件,可以使我们使用现有的 go test 工具编译 pouch daemon ,当运行如下命令时,go test 将编译当前路径下以 _test 结尾的文件所属的 package,即我们需要的 main package,然后链接go test 提供的测试主程序中(即前面提到的 _testmain.go  文件),生成测试可执行文件

# go test -c -race -cover -covermode=atomic -o pouchd-test -coverpkg $pkgs

其中 \$pkg 指定需要统计测试覆盖率的包名,go test 调用 cover 工具对指定的 package 源码重写,加入测试覆盖率计数器;-o 参数指示仅编译不运行,且指定测试二进制名为 pouchd-test。执行上述命令后,即可得到一个调用 main() 函数的测试二进制文件

第三步,启动 pouch-test 运行测试代码,由于测试代码调用 pouch daemon 的入口 main() 函数,即可达到启动 pouch daemon 并提供服务的目的。具体命令如下:

# pouchd-test -test.coverprofile=$DIR/integrationcover.out DEVEL --debug

其中,-test 前缀的参数由 go test 处理,DEVEL 之后的参数,则会传递给 main() 函数。此时,正常执行测试用例,测试结束后杀掉 pouchd-test 进程,go test 工具会打印出测试覆盖率,并生成覆盖率文件,完成集成测试覆盖率的统计

从上述步骤可以看到,统计集成测试覆盖率的主要工作在于提供一个 main_test.go 文件,接下来我们分析一下这个文件做了哪些工作。

首先,文件中定义了一个测试函数 TestMain() ,这是入口函数,执行测试可执行文件时,会调用这个函数

函数中 16-27 行进行了参数处理,过滤 -test 开头以及 DEVEL 参数,并将余下参数全部赋值给 os.Args 。这是因为 go test 认将第一个非破折号 - 开头的参数,交由测试函数处理,main_test.go 代码中,过滤参数并重新赋值 os.Args,将参数传给 main() 函数,使得我们可以如常使用 daemon 参数。

第 28-31 行调用 main 函数,启动 daemon 服务。第 33-40 行,接收指定信号并直接退出。注意,我们还定义了一个 waitCh channel ,用于 main  函数退出时,通知测试函数退出,以防止出现 main  函数调用自身而其引起的程序永不退出问题。

有关集成测试覆盖率统计实现方法,还可以参考这篇文章 《Generating Coverage Profiles for Golang Integration Tests》(https://www.cyphar.com/blog/post/20170412-golang-integration-coverage)。

结语

集成测试覆盖率的统计,需要灵活运用 Golang 提供的工具,并根据自身项目代码特点适配测试文件。加入集成测试覆盖率统计后,PouchContainer 的覆盖率从仅统计单测时的 18% 提升至 60%,这将更准确展示测试现状。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐