一、引言
我是一名中間件 QA,我對應的研發團隊是有贊 PaaS,目前我們團隊有很多產品是使用 go 語言開發,因此我對 go 語言項目的單測覆蓋率、集成以及增量測試覆蓋率統計與分析做了探索。
二、單測覆蓋率以及靜態代碼分析
2.1 單測覆蓋率分析
Go 語言自身提供了單元測試工具 go test
,單元測試文件必須以 *_test.go
形式存在, go test
工具同時也提供了分析單測覆蓋率的功能。因爲需要將單測覆蓋率上傳到 sonar 平臺展示,所以必須將覆蓋率文件轉換成能被 sonar 識別的格式,因此,還需要另外一個命令行工具 gocov。首先我們使用 go test
生成覆蓋率輸出文件 cover.out
,並通過 gocov 工具來將生成的覆蓋率文件 cover.out
轉換成可以被 sonar 識別的 Cobertura 格式的 xml 文件。如下所示:
go test -v ./... -coverprofile=cover.out #生成覆蓋率輸出
gocov convert cover.out | gocov-xml > coverage.xml #將覆蓋率輸出轉換成xml格式的報告
將生成的單測覆蓋率報告發送到 sonar 平臺上來展示。
2.2 靜態代碼分析
Go 靜態代碼分析工具有兩個,分別是 gometalinter 和 golangci-lint,我們現在使用的是 golangci-lint,因爲 gometalinter 已經停止維護,而且作者也推薦去使用 golangci-lint。
2.2.1 golangci-lint 的安裝
以下是安裝 golangci-lint 推薦的兩種方法:
- 將二進制文件安裝在 (go env GOPATH)/bin/golangci-lint 目錄下
curl-sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin vX.Y.Z
- 或者將二進制文件安裝在 ./bin/ 目錄下
curl-sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s vX.Y.Z
安裝完成之後可以通過使用 golangci-lint--version
來查看它的版本。
2.2.2 golangci-lint 的使用
在需要進行靜態代碼掃描的目錄下執行 golangci-lint run
,此命令和 golangci-lint run./…
命令等效,表示掃描整個項目文件代碼,並進行監測,也可以通過指定 go 文件或者文件目錄名來對特定的代碼文件或者目錄進行代碼掃描,例如 golangci-lint run dir1 dir2/...dir3/file1.go
。
ps:掃描指定目錄的時候是不支持遞歸掃描的,如果要進行遞歸掃描需要在目錄路徑後面追加
/…
默認情況下 golangci-lint 只啓用以下的 linters:
Enabled by default linters:
deadcode: 發現沒有使用的代碼
- errcheck: 用於檢查 go 程序中有 error 返回的函數,卻沒有做判斷檢查
- gosimple: 檢測代碼是否可以簡化
- govet (vet, vetshadow): 檢查 go 源代碼並報告可疑結構,例如 Printf 調用,其參數與格式字符串不一致
- ineffassign: 檢測是否有未使用的代碼、變量、常量、類型、結構體、函數、函數參數等
- staticcheck: 提供了巨多的靜態檢查,檢查 bug,分析性能等
- structcheck:發現未使用的結構體字段
- typecheck: 對 go 代碼進行解析和類型檢查
- unused: 檢查未使用的常量,變量,函數和類型
- varcheck: 查找未使用的全局變量和常量
Disabled by default linters:
bodyclose: 對 HTTP 響應是否 close 成功檢測
- dupl: 代碼克隆監測工具
- gochecknoglobals: 檢查 go 代碼中是否存在全局變量
- goimports: 做所有 gofmt 做的事. 此外還檢查未使用的導入
- golint: 打印出 go 代碼的格式錯誤
- gofmt: 檢測代碼是否都已經格式化, 默認情況下使用
-s
來檢查代碼是否簡化 -
- ……………………………
未啓用的還有很多工具,可以通過使用 golangci-lint help linters
命令查看還有哪些工具可以使用,如果想要啓用沒有默認開啓的工具,可以在執行命令時使用 -E
參數來啓用,比如要啓用 golint 的話,只需要執行一下命令 golangci-lint run-E=golint
。除了用 -E
來啓動參數外,還可以指定最長執行時間 —deadline
、跳過要掃描的目錄 --skip-dirs
等等。如果要了解更多,請使用 golangci-lint run-h
來查看。
特別注意 —-exclude-use-default
參數,golangci-lint 對於上面默認的啓用 linters 中做了一些過濾措施,比如對於 errcheck
,它不會掃描 ((os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*printf?|os\.(Un)?Setenv)
這些函數返回的 error 是否被 checked,所以如果代碼中使用到這些函數,並且沒有接收 error 的話是不會被掃描到的。類似的還有golint
、 govet
、staticcheck
、 gosec
需要注意。如果想要不過濾這些就需要使用 --exclude-use-default=false
來啓用。
2.3 接入sonar
go 接入 sonar 需要 sonar-scanner 工具以及 sonar-project.properties 文件。
2.3.1 sonar-scanner
sonar-scanner 是 sonar 官方提供的代碼掃描器,下載地址是 https://docs.sonarqube.org/display/SCAN/Analyzing+with+SonarQube+Scanner
。下載好之後解壓,解壓後的目錄下有四個文件夾,分別是 bin、conf、jre、lib,然後將 bin 文件夾路徑添加到 $PATH 環境變量下,使用 sonar-scanner-v
來查看版本。
2.3.2 sonar-project.properties
sonar-project.properties 文件的作用主要是配置 sonar 掃描器掃描哪些類型的文件以及文件目錄,最後將報表結果上報到 sonar 服務器,sonar-project.propertie 內容如下:
內容如下:
#sonar安裝的服務器地址
sonar.host.url=http://ip:port
#服務器賬號
sonar.login=root
#服務器密碼
sonar.password=root
#項目使用的語言
sonar.language=go
#項目的獨特關鍵字,maven 項目是 <groupId>:<artiactId>,go 項目自己定義就可以
sonar.projectKey=projectKey
#將在web界面上顯示的名字
sonar.projectName=demo
#項目版本
sonar.projectVersion=1.0
#需要分析的源碼目錄的路徑
sonar.sources=.
sonar.exclusions=**/*_test.go,**/vendor/**
sonar.tests=.
sonar.test.inclusions=**/*_test.go
sonar.test.exclusions=**/vendor/**
#golangci-lint 報告路徑
sonar.go.golangci-lint.reportPaths=report.xml
#單測覆蓋率報告地址
sonar.go.coverage.reportPaths=cover.out
在項目目錄下分別執行go test-v./...-coverprofile=cover.out
以及 golangci-lint run--out-format checkstyle./...>report.xml
等生產報告,並執行sonar-scan 來將生成的報告上傳到服務器。這裏默認在使用的是sonar8.1 已經支持了 golangci-lint
報告主頁
三、集成測試覆蓋率分析
對於 Go 項目沒有類似 java jacoco 這樣的第三方測試工具,就算是開源的第三方工具,一般單元測試執行以及單測覆蓋率分析都是使用 Go 自帶的測試工具 go test
來執行的。
閱讀了GO的官方博客之後發現其實針對二進制文件是有類似的工具 gcov。在文章中作者也說了,對於在 go 1.2 之前,其實也是使用類似 gcov 的方式對二進制程序在分支上設置斷點,在每個分支執行時,將斷點清除並將分支的目標語句標記爲 “covered” 。
但是通過文章可以知道,在 go 1.2 之後是不支持使用此種方式,而且也不推薦使用 gcov 來統計覆蓋率,因爲執行二進制分析是很有挑戰且很困難的,它還需要一種可靠的方式來執行跟蹤綁定到源代碼,這也很困難,這些問題包括不準確的調試信息和類似內聯函數使分析複雜化,最重要的是,這種方法非常不便攜。
3.1 解決方法
通過查找資料,發現了一個並不完美但是可以解決這個問題的方法。go test 中有一個 -c 的 flag,可以將單測的代碼和被單測調用的代碼編譯成二進制包執行,但是這種方式並沒有將整個項目的代碼包含進去,不過可以通過增加一個測試文件 main_test.go,文件內容如下:
func TestMainStart(t *testing.T) {
var args []string
for _, arg := range os.Args {
if !strings.HasPrefix(arg, "-test") {
args = append(args, arg)
}
}
os.Args = args
main()
}
將主函數放在此測試代碼中,由於 Go 的入口函數是 main 函數,所以這樣就會將整個 Go 項目都打包成一個已經插樁的二進制文件,如果項目啓動的時候需要傳入參數,則會將其中程序啓動時傳入的不是 -test標記的參數放入到os.Args 中傳遞給main 函數。以上代碼也可以自己在測試文件中增加消息通知監聽,來退出測試函數。當集成測試跑完後就可以得到覆蓋率代碼,整個流程可參考下圖:
#第一步:執行集成測試,並將此函數編譯成二進制文件
go test -coverpkg="./..." -c -o cover.test
#第二步:運行二進制文件,指定運行的測試方法是 TestMainStart,並將覆蓋率報告輸出
./cover.test -test.run "TestMainStart" -test.coverprofile=cover.out
#第三步:將輸出的覆蓋率報告轉換成 html 文件(html 文件查看效果比較好)
go tool cover -html cover.out -o cover.html
#第四步:生成 Cobertura 格式的 xml 文件
gocov convert cover.out | gocov-xml > cover.xml
3.2 缺點
- 必須所有 Go 語言項目中新增一個這樣的測試代碼文件,纔可以使用
- 必須退出進程纔可以獲得報告,但是如果測試程序是在 k8s 的 pod 中,一旦程序退出,pod 就會自動退出無法獲取到文件
- 想要得到測試覆蓋率數據不能像 jacoco 那樣直接調用接口可以 dump 到本地,程序必須增加一個接收信號量的參數,保證主函數的退出,不然集成測試代碼跑完,覆蓋率信息是不會寫到磁盤的
- 由於上面的原因,報告儲存在遠端,無法下載到當前 Jenkins 上,要去遠端 dump 文件下來分析
- 不能將分佈式的應用的數據結合起來之後做全量統計(只能跑單個應用)
以上缺陷在有贊paas團隊通過一些不是特別優雅的方式解決,以下是解決方案
3.3 優化
ps:由於當前有贊 PaaS 的 ci 環境是在 k8s 集羣中實現的,所以這裏就針對 k8s中 的優化方案
3.3.1 針對編譯前需要新增一個測試文件,包裹main函數
測試函數也是要求所有項目中增加一個測試文件,或者 Jenkins 編譯部署鏡像之前在 pipline 中生成一個文件
3.3.2 針對以上必須程序退出纔可以或許到測試覆蓋率報告的缺點:
假設 k8s 基礎鏡像中已經裝好 python,我在啓動 pod 的時候默認啓動兩個服務,一個是被測試的服務,一個是 python 啓動的 http 服務。
然後將項目服務的啓動寫入腳本中,並在 deployment 中通過 nohup 啓動服務,並再啓動一個 python 服務
spec:
containers:
- command:
- /bin/bash
- -c
- (nohup /data/project/start.sh &);(cd python && -m SimpleHTTPServer 12345)
image: $imageAddress
殺死項目服務後,因爲還有 python 服務在,pod 不會退出,可以拿到覆蓋率測試報告
3.3.3 覆蓋率報告在遠端,如何在跑完Jenkins任務後來直接獲取到報告:
可以在跑集成測試後通過執行 http 請求來獲取容器內的 cover.out,比如 wget http://{ip}:{port}/{path}/cover.out
,並將此覆蓋率報告編譯成 Cobertura 格式的 xml,放入到 Jenkins 中統計。
如果是執行了多個服務端,需要合併覆蓋率報告,可以使用 gocovmerge
3.3.4 如何在k8s中自動化kill程序讓其退出:
對於退出程序可以直接在集成測試代碼中使用 kubectl 命令將 pod 中的程序 kill
pid=`kubectl exec $podname -c $container -n dts -- ps -ef | grep $process | grep -v grep | awk '{print $2}'`
kubectl exec $podname -c $container -n $namespace -- kill $pid
3.4 jenkins 報告
四、集成測試增量覆蓋率分析
4.1 diff_cover
增量覆蓋率分析我們選擇了開源工具 diffover,diffcover 是用 python 開發,通過 git diff 來對比當前分支和需要比對的分支,主要針對新增代碼做覆蓋率分析。
4.2 安裝
安裝 diff_cover的機器需要有 python 的環境,有兩種安裝方式:
1、通過pip 來直接下載安裝
pip install diff_cover
2、通過源代碼安裝
pip install diff_covers
4.3 使用方式
ps:必須在需要對比的項目目錄下運行!!!
4.3.1 生成單元測試覆蓋率報告
go test-v./...-coverprofile=cover.outgocov convert cover.out|gocov-xml>coverage.xml
4.3.2 增量覆蓋率分析
diff-cover coverage.xml--compare-branch=xxxx--html-report report.html
–compare-branch:是選擇需要對比的分支號
–html-report:是將增量測試報告生成 html 的報告模式
除了以上參數,此工具還有很多其他參數,比如
–fail-under:覆蓋率低於某個值,返回非零狀態代碼
–diff-range-notation:設置 diff 的範圍,就是git diff{compare-branch}{diff-range-notation}
的作用等等。
具體可以通過diff_cover-h
來獲得更多詳細的信息
4.4 報告
- 命令行展示
- HTML展示
本文轉載自公衆號有贊coder(ID:youzan_coder)。
原文鏈接: