您好,登錄后才能下訂單哦!
作者| 阿里云智能事業群高級測試開發工程師 劉璐
最初,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
文件的第一行標識覆蓋率統計模式為 set
,go 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 的實現細節。
make unit-test
命令,即可實現覆蓋率統計收集。單測覆蓋率統計的實現可以可以參考 Makefile。需要注意的是,覆蓋率統計時需要排除一些無關 package,例如 vendor 目錄、types 目錄等,否則會影響測試覆蓋率的準確性。go build
編譯,源碼中沒有插入計數器,無法統計測試覆蓋率。實現統計 pouch daemon 的測試覆蓋率的 PR 參見https://github.com/alibaba/pouch/pull/1338),這個 PR(由于代碼的不斷迭代,最新的代碼位置已改變,請讀者參照本文所對應的 commit 代碼)中,我們做了如下工作:
根目錄下新增 main_test.go 測試文件
hack/build 腳本中,新增 testserver 函數用于編譯 main package,生成可執行測試文件
hack/make.sh 腳本中,后臺啟動步驟 2 生成的測試文件,并運行 API 和命令行測試
測試結束后,給測試進程發送信號,并收集測試覆蓋率
接下來將詳細講述實現細節,首先,新增 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%,這將更準確展示測試現狀。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。