溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

PouchContainer 集成測(cè)試覆蓋率統(tǒng)計(jì)

發(fā)布時(shí)間:2020-08-11 15:59:30 來源:ITPUB博客 閱讀:184 作者:阿里系統(tǒng)軟件技術(shù) 欄目:服務(wù)器

PouchContainer 集成測(cè)試覆蓋率統(tǒng)計(jì)

作者| 阿里云智能事業(yè)群高級(jí)測(cè)試開發(fā)工程師 劉璐

PouchContainer 是阿里巴巴開源的富容器技術(shù),已于 2018 年 9 月正式發(fā)布 GA 版本,已經(jīng)完全達(dá)到生產(chǎn)級(jí)別。PouchContainer 一直非常重視項(xiàng)目質(zhì)量,項(xiàng)目的開發(fā)者需在提交 PR 時(shí)提供與之對(duì)應(yīng)的單測(cè)與集成測(cè)試代碼。這種要求,一方面保證回歸質(zhì)量,同時(shí)也減少代碼 review 成本,提高合作效率。(更多參考:PouchContainer 開源版本及內(nèi)部版本一致性實(shí)踐)

最初,PouchContainer 結(jié)合 TravisCI 與 Codecov 工具,為每次 PR 提交運(yùn)行測(cè)試并展示單元測(cè)試覆蓋率。對(duì)于一些添加集成測(cè)試的 PR,集成測(cè)試的增減所帶來的測(cè)試覆蓋率變化并沒有納入到測(cè)試覆蓋率的統(tǒng)計(jì)中。

集成測(cè)試覆蓋率的缺失,使得開發(fā)者缺少對(duì)項(xiàng)目測(cè)試覆蓋率的更完整認(rèn)知。為了更全面的展示 PouchContainer 的測(cè)試覆蓋率,現(xiàn)在 PouchContainer 已經(jīng)加入了集成測(cè)試覆蓋率的統(tǒng)計(jì)功能。本文主要介紹集成測(cè)試覆蓋率統(tǒng)計(jì)在 PouchContainer 中的實(shí)現(xiàn)。

Go 測(cè)試覆蓋率

在介紹集成測(cè)試覆蓋率統(tǒng)計(jì)實(shí)現(xiàn)之前,我們需要了解 Golang 的覆蓋率統(tǒng)計(jì)的原理。Golang 的覆蓋率統(tǒng)計(jì),是通過在編譯之前重寫包的源代碼,加入統(tǒng)計(jì)信息,然后編譯、運(yùn)行、收集測(cè)試覆蓋率。有關(guān) Go 測(cè)試覆蓋率的原理可參考 The cover story (https://blog.golang.org/cover),接下來的內(nèi)容,主要參考上述文章,并具體列出執(zhí)行過程。

首先,給出一個(gè)待測(cè) Size() 函數(shù),它有多個(gè) 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"
}

對(duì)應(yīng)的測(cè)試代碼如下:

$ 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)
        }
    }
}


執(zhí)行 go test -x -cover -coverprofile=./size.out 命令,運(yùn)行測(cè)試并統(tǒng)計(jì)測(cè)試覆蓋率。其中,-x 參數(shù)打印上述命令的執(zhí)行過程(需注意:打印的執(zhí)行步驟信息不完整,如果手動(dòng)執(zhí)行輸出的步驟,則會(huì)運(yùn)行失敗,這是因?yàn)?go test 的一些執(zhí)行步驟并沒有打印信息),-cover 參數(shù)開啟測(cè)試覆蓋率統(tǒng)計(jì)功能,-coverprofile 參數(shù)指定存儲(chǔ)測(cè)試覆蓋率文件,運(yùn)行結(jié)果如下:


$ 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

從上述輸出的倒數(shù)第二行可知,測(cè)試覆蓋率為 60%。分析 go test 的執(zhí)行步驟,第五行調(diào)用 /usr/local/go/pkg/tool/darwin_amd64/cover 工具,這個(gè)工具重寫待測(cè)源碼,在代碼中加入計(jì)數(shù)點(diǎn),用以統(tǒng)計(jì)測(cè)試覆蓋率。第 8-13 行編譯待測(cè)文件和 _testmain.go 文件(這個(gè)文件是 go test 工具生成的,具體實(shí)現(xiàn)細(xì)節(jié)可以參見https://github.com/golang/go/blob/3f150934e274f9ce167e1ed565fb3e60b8ea8223/src/cmd/go/internal/test/test.go#L1887),生成 test.test 測(cè)試執(zhí)行文件。第 13 行,執(zhí)行 test.test 測(cè)試文件,傳入測(cè)試相關(guān)參數(shù),即可運(yùn)行測(cè)試。

查看 cover 命令的幫助信息,再次執(zhí)行 cover 命令,可以查看被重寫后的測(cè)試代碼:

$ 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{
        340x9001a// [0]
        12120x130002// [1]
        560x14000d// [2]
        780x10000e// [3]
        9100x11000e// [4]
    },
    NumStmt: [5]uint16{
        1// 0
        1// 1
        1// 2
        1// 3
        1// 4
    },
}

查看 go test 運(yùn)行測(cè)試后的覆蓋率統(tǒng)計(jì)文件,信息如下:

$ 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

文件的第一行標(biāo)識(shí)覆蓋率統(tǒng)計(jì)模式為 set,go test 提供 set、count、atomic 三種模式:

  • set 模式僅統(tǒng)計(jì)語句是否運(yùn)行;

  • count 模式統(tǒng)計(jì)語句運(yùn)行的次數(shù);

  • atomic 模式與 count 類似,統(tǒng)計(jì)語句運(yùn)行次數(shù),適用于多線程測(cè)試。

第二行開始的格式為:name.go:line.column,line.column numberOfStatements count,即文件名、代碼的起始位置、語句的行數(shù)以及被運(yùn)行的次數(shù)。本次示例代碼中,待統(tǒng)計(jì)的語句共 5 行,統(tǒng)計(jì)模式為 set,共有 3 個(gè) count 被置為 1(讀者可以將 covermode 設(shè)置為 count,觀察 count 輸出有何變化),所以最終的測(cè)試覆蓋率結(jié)果為 60%。

PouchContainer 測(cè)試覆蓋率

PouchContainer 集成 CodeCov 工具,每次運(yùn)行 TravisCI 會(huì)將測(cè)試覆蓋率文件上傳至 CodeCov 網(wǎng)站,完成覆蓋率的可視化展示與持續(xù)追蹤。

TravisCI 與 CodeCov 可以很容易的集成,只需在測(cè)試路徑下生成一個(gè) coverage.txt 名字的覆蓋率統(tǒng)計(jì)文件,并在 .tarvis.yml 文件中調(diào)用 CodeCov 的腳本,即可上傳覆蓋率統(tǒng)計(jì)文件,具體命令可以參考 Makefile 中 TEST_FLAGS= make build-integration-test 里面的實(shí)現(xiàn),感興趣的同學(xué)也可以直接查看 CodeCov 腳本,了解其實(shí)現(xiàn)細(xì)節(jié)。

接下來,我們從單測(cè)和集成測(cè)試覆蓋率統(tǒng)計(jì)兩方面展開,詳細(xì)闡述 PouchContainer 的實(shí)現(xiàn)細(xì)節(jié)。

單測(cè)覆蓋率統(tǒng)計(jì)

PouchContianer 收集單測(cè)覆蓋率相對(duì)簡(jiǎn)單,只需要執(zhí)行 make unit-test 命令,即可實(shí)現(xiàn)覆蓋率統(tǒng)計(jì)收集。單測(cè)覆蓋率統(tǒng)計(jì)的實(shí)現(xiàn)可以可以參考 Makefile。需要注意的是,覆蓋率統(tǒng)計(jì)時(shí)需要排除一些無關(guān) package,例如 vendor 目錄、types 目錄等,否則會(huì)影響測(cè)試覆蓋率的準(zhǔn)確性。

集成測(cè)試覆蓋率統(tǒng)計(jì)

PouchContainer 集成測(cè)試,是通過啟動(dòng) pouch daemon,然后執(zhí)行 pouch 命令行或者直接發(fā)送 API 請(qǐng)求,實(shí)現(xiàn)對(duì) daemon API 和命令行的測(cè)試。正常情況下,待測(cè)試 pouch daemon 是通過 go build編譯,源碼中沒有插入計(jì)數(shù)器,無法統(tǒng)計(jì)測(cè)試覆蓋率。

實(shí)現(xiàn)統(tǒng)計(jì) pouch daemon 的測(cè)試覆蓋率的 PR 參見https://github.com/alibaba/pouch/pull/1338),這個(gè) PR(由于代碼的不斷迭代,最新的代碼位置已改變,請(qǐng)讀者參照本文所對(duì)應(yīng)的 commit 代碼)中,我們做了如下工作:

  1. 根目錄下新增 main_test.go 測(cè)試文件

  2. hack/build 腳本中,新增 testserver 函數(shù)用于編譯 main package,生成可執(zhí)行測(cè)試文件

  3. hack/make.sh 腳本中,后臺(tái)啟動(dòng)步驟 2 生成的測(cè)試文件,并運(yùn)行 API 和命令行測(cè)試

  4. 測(cè)試結(jié)束后,給測(cè)試進(jìn)程發(fā)送信號(hào),并收集測(cè)試覆蓋率

接下來將詳細(xì)講述實(shí)現(xiàn)細(xì)節(jié),首先,新增 main_test.go 測(cè)試文件,并在文件中定義一個(gè)測(cè)試函數(shù) 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 int1)

    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 文件,可以使我們使用現(xiàn)有的 go test 工具編譯 pouch daemon ,當(dāng)運(yùn)行如下命令時(shí),go test 將編譯當(dāng)前路徑下以 _test 結(jié)尾的文件所屬的 package,即我們需要的 main package,然后鏈接到 go test 提供的測(cè)試主程序中(即前面提到的 _testmain.go  文件),生成測(cè)試可執(zhí)行文件:

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

其中 \$pkg 指定需要統(tǒng)計(jì)測(cè)試覆蓋率的包名,go test 調(diào)用 cover 工具對(duì)指定的 package 源碼重寫,加入測(cè)試覆蓋率計(jì)數(shù)器;-o 參數(shù)指示僅編譯不運(yùn)行,且指定測(cè)試二進(jìn)制名為 pouchd-test。執(zhí)行上述命令后,即可得到一個(gè)調(diào)用 main() 函數(shù)的測(cè)試二進(jìn)制文件。

第三步,啟動(dòng) pouch-test 運(yùn)行測(cè)試代碼,由于測(cè)試代碼中調(diào)用 pouch daemon 的入口 main() 函數(shù),即可達(dá)到啟動(dòng) pouch daemon 并提供服務(wù)的目的。具體命令如下:

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

其中,-test 前綴的參數(shù)由 go test 處理,DEVEL 之后的參數(shù),則會(huì)傳遞給 main() 函數(shù)。此時(shí),正常執(zhí)行測(cè)試用例,測(cè)試結(jié)束后殺掉 pouchd-test 進(jìn)程,go test 工具會(huì)打印出測(cè)試覆蓋率,并生成覆蓋率文件,完成集成測(cè)試覆蓋率的統(tǒng)計(jì)。

從上述步驟可以看到,統(tǒng)計(jì)集成測(cè)試覆蓋率的主要工作在于提供一個(gè) main_test.go 文件,接下來我們分析一下這個(gè)文件做了哪些工作。

首先,文件中定義了一個(gè)測(cè)試函數(shù) TestMain() ,這是入口函數(shù),執(zhí)行測(cè)試可執(zhí)行文件時(shí),會(huì)調(diào)用這個(gè)函數(shù)。

函數(shù)中 16-27 行進(jìn)行了參數(shù)處理,過濾 -test 開頭以及 DEVEL 參數(shù),并將余下參數(shù)全部賦值給 os.Args 。這是因?yàn)?go test 默認(rèn)將第一個(gè)非破折號(hào) - 開頭的參數(shù),交由測(cè)試函數(shù)處理,main_test.go 代碼中,過濾參數(shù)并重新賦值 os.Args,將參數(shù)傳給 main() 函數(shù),使得我們可以如常使用 daemon 參數(shù)。

第 28-31 行調(diào)用 main 函數(shù),啟動(dòng) daemon 服務(wù)。第 33-40 行,接收指定信號(hào)并直接退出。注意,我們還定義了一個(gè) waitCh channel ,用于 main  函數(shù)退出時(shí),通知測(cè)試函數(shù)退出,以防止出現(xiàn) main  函數(shù)調(diào)用自身而其引起的程序永不退出問題。

有關(guān)集成測(cè)試覆蓋率統(tǒng)計(jì)的實(shí)現(xiàn)方法,還可以參考這篇文章 《Generating Coverage Profiles for Golang Integration Tests》(https://www.cyphar.com/blog/post/20170412-golang-integration-coverage)。

結(jié)語

集成測(cè)試覆蓋率的統(tǒng)計(jì),需要靈活運(yùn)用 Golang 提供的工具,并根據(jù)自身項(xiàng)目代碼特點(diǎn)適配測(cè)試文件。加入集成測(cè)試覆蓋率統(tǒng)計(jì)后,PouchContainer 的覆蓋率從僅統(tǒng)計(jì)單測(cè)時(shí)的 18% 提升至 60%,這將更準(zhǔn)確展示測(cè)試現(xiàn)狀。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI