Categories
程式開發

有贊 GO 項目單測、集成、增量覆蓋率統計與分析


一、引言

我是一名中間件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 的話是不會被掃描到的。類似的還有golintgovetstaticcheckgosec需要注意。如果想要不過濾這些就需要使用 --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 项目是 :,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 項目單測、集成、增量覆蓋率統計與分析 1

三、集成測試覆蓋率分析

對於 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 項目單測、集成、增量覆蓋率統計與分析 2

#第一步:执行集成测试,并将此函数编译成二进制文件
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 缺點

  1. 必須所有 Go 語言項目中新增一個這樣的測試代碼文件,才可以使用
  2. 必須退出進程才可以獲得報告,但是如果測試程序是在 k8s 的 pod 中,一旦程序退出,pod 就會自動退出無法獲取到文件
  3. 想要得到測試覆蓋率數據不能像jacoco 那樣直接調用接口可以dump 到本地,程序必須增加一個接收信號量的參數,保證主函數的退出,不然集成測試代碼跑完,覆蓋率信息是不會寫到磁盤的
  4. 由於上面的原因,報告儲存在遠端,無法下載到當前 Jenkins 上,要去遠端 dump 文件下來分析
  5. 不能將分佈式的應用的數據結合起來之後做全量統計(只能跑單個應用)

以上缺陷在有贊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 報告

有贊 GO 項目單測、集成、增量覆蓋率統計與分析 3

四、集成測試增量覆蓋率分析

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 報告

  1. 命令行展示

有贊 GO 項目單測、集成、增量覆蓋率統計與分析 4

  1. HTML展示

有贊 GO 項目單測、集成、增量覆蓋率統計與分析 5

本文轉載自公眾號有贊coder(ID:youzan_coder)。

原文鏈接

https://mp.weixin.qq.com/s/mNGkMggkkuRSuflHw3vIyA