PouchContainer 整合測試覆蓋率統計

阿里系統軟體技術發表於2019-03-21

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 工具生成的,具體實現細節可以參見),生成 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{
        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 執行測試後的覆蓋率統計檔案,資訊如下:

$ 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 參見),這個 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 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 檔案,可以使我們使用現有的 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%,這將更準確展示測試現狀。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31555606/viewspace-2638998/,如需轉載,請註明出處,否則將追究法律責任。

相關文章