Go應用單元測試實踐

一  背景

高德打車運營的應用大多基於go進行開發的,我們希望在預整合環境下,當研發部署完程式碼,能自動觸發單元測試和介面自動化測試,並生成覆蓋率報告。參考了許多篇關於go單元測試的文章,有的缺少行增量覆蓋率,有的缺少case執行結果/case執行日誌。
本文旨在搭建一個穩定執行且維護成本低的單元測試/整合測試環境。

二  單元測試

1  單測執行概述

圖1 單測執行流程圖
aone作為阿里巴巴集團數字化研發協同平臺,本身提供了各種整合測試實驗室,實驗室中可以執行自定義指令碼。如圖1所示,為單元測試執行流程圖。單元測試由aone實驗室指令碼觸發,Java服務收到單測任務後調起單測指令碼並執行,最後由aone實驗室輪詢執行結果。之所以不在單測實驗室指令碼中直接執行單測,主要存在以下兩個原因。一是單測的執行依賴GO環境,以及一些生成覆蓋率檔案所需的三方工具。目前aone實驗室不支援自定義映象接入,每次執行都需要安裝環境,安裝環境的耗時遠大於執行單測。二是每個應用的單測執行命令可能不太一樣,一旦應用數目較多,如果單測指令碼需要調整,更改的成本比較高。因此啟動一個JAVA服務(完全可以複用已有的服務,降低成本),將執行單測所需要的指令碼,以及環境都打包在這個服務上。aone上的實驗室指令碼,只進行單測任務的下發、輪詢和執行結果的展示。具體流程如下:
  1. 當開發在預整合環境提交程式碼、部署完成之後,流程自動執行單測實驗室。單測實驗室裡的指令碼,先呼叫任務下發介面/unit/taskReceive,這時Java服務會呼叫對應的單測指令碼。
  2. 由於單測指令碼執行時間會比較長,所以/unit/taskReceive介面會超時。在單測指令碼正在執行的時候,單測實驗室的指令碼會一直呼叫/unit/taskQuery介面,查詢此次單測任務的狀態,直到返回正確結果為止。
  3. 當單測指令碼完成時,會回撥任務完成介面/unit/taskSave介面,將結果存起來。這樣單測實驗室指令碼再呼叫/unit/taskQuery介面查詢時,就會返回此次單測的結果。
  4. 單測實驗室指令碼,根據任務返回的結果,將單測結果解析、展示。

2  環境搭建

將所需的環境,打包到Java服務的docker中:
  • golang安裝
go單測需要執行go test,所以需要在環境中安裝go。安裝完成後,配置環境變數和代理。
wget https://golang.google.cn/dl/go1.17.8.linux-amd64.tar.gztar -zxvf go1.17.8.linux-amd64.tar.gz -C /usr/local/mkdir -p /${your go path dir}/gopathecho -e "export PATH=\"$PATH:/usr/local/go/bin:/${your go path dir}/gopath/bin\"\nexport GOPATH=\"/${your go path dir}/gopath\"\nexport GOPROXY=\"${go代理地址},direct\"" >> /etc/profilesource /etc/profile
  • 程式碼覆蓋率外掛安裝
運用一些開源工具,將單測生成的覆蓋檔案轉換成xml/html格式的覆蓋率檔案。主要用到gocov-html,gocov,gocov-xml。參考地址[1][2]
go get github.com/matm/gocov-htmlgo get github.com/axw/gocov/... go get github.com/AlekSi/gocov-xml
  • 行增量覆蓋率工具安裝
利用diff-cover[3],生成行增量覆蓋率。diff-cover依賴python3,python3的安裝可能需要先裝好gcc,automake,autoconf,libtool,make,zlib,zlib-devel openssl。
yum -y install gcc automake autoconf libtool make zlib zlib-devel openssl openssl-develwget https://www.python.org/ftp/python/3.8.1/Python-3.8.1.tgztar -zxvf Python-3.8.1.tgz && cd Python-3.8.1 && ./configure && make && make install pip3 install diff-cover -i https://mirrors.aliyun.com/pypi/simpl
  • git安裝&配置
執行單元測試時,依賴開發的程式碼。需要配置好一個有程式碼許可權的git ssh公鑰和私鑰,用來下載程式碼。
yum -y git name=`git config user.name`if [ -z "$name" ]then git config --global user.name "xxx" git config --global user.email "[email protected]" mkdir -p ~/.ssh cp ${your id_rsa} ~/.ssh/fi

3  Java服務實現

單測任務下發介面

Path:/unit/taskReceiveMethod:POSTParams:{"taskId": "123456", //可以用日期20220221102104,主要用來標識此次單測"appName":"應用A", //應用名,根據應用名,選擇執行對應的單測指令碼。比如應用A就會執行應用A.sh"branch":"releases/test-branch-code", //需要執行單測的分支名"repo":"[email protected]"//應用A的程式碼地址,下載程式碼之後,才能執行單測}Result:返回啥都行,反正會超時。
具體實現邏輯:
  • 在redis中記錄此次單測任務,key:"${appName}${taskId}-unit",value:"ongoing"。以便/unit/taskQuery查詢,從而知道單測還在執行中。
  • 根據appName引數,選擇執行${appName}.sh指令碼。如果指令碼不存在,就去阿里雲物件儲存服務(Object Storage Service,簡稱OSS)下載指令碼(所以,如果單測指令碼有更新,就更新下OSS上的指令碼,然後刪除執行機器上的${appName}.sh即可。這樣可以不重新部署Java服務,即可更改執行指令碼)。${appName}.sh指令碼大致邏輯如下:
source /etc/profileAPP_NAME=$1Branch=$2TaskId=$3Repo=$4DIR=`pwd`PREFIX=$APP_NAME$TaskId#生成覆蓋率檔案的資料夾mkdir -p $DIR/$APP_NAME/$TaskId/coverCOVER_FILE=$DIR/$APP_NAME/$TaskId/cover/core.coverLOG_FILE=$DIR/$APP_NAME/$TaskId/cover/log.txtCOVER_DIR=$DIR/$APP_NAME/$TaskId/coverUNIT_TEST_RESULT_FILE=$DIR/$APP_NAME/$TaskId/cover/unit_pass.txt#存放覆蓋率詳情html檔案的資料夾mkdir -p /${your path}/res_unit#下載程式碼cd$DIR/$APP_NAME/$TaskIdgit clone -b $Branch$Repo#執行單元測試cd ./$APP_NAMECONF_DIR=$DIR/$APP_NAME/$TaskId/$APP_NAME/confgo test ./... -timeout 3m -v -gcflags=-l -cover=true -coverprofile=$COVER_FILE -mod=vendor -args --confDir=$CONF_DIR >> $LOG_FILE#行增量覆蓋率gocov convert $COVER_FILE | gocov-xml > $COVER_DIR/coverage.xmldiff-cover $COVER_DIR/coverage.xml --compare-branch=origin/master --html-report $COVER_DIR/report.html > $COVER_DIR/diff.outtmp=`cat $COVER_DIR/diff.out | grep "Total:" | cut -d ':' -f2`if [ -n "$tmp" ]thenecho"CODE_COVERAGE_NAME_UPDATELINES : 行增量" CODE_COVERAGE_UPDATE_LINES_TOTAL=`cat $COVER_DIR/diff.out | grep "Total:" | cut -d':' -f2 | grep -o -E '[0-9]+'` miss=`cat $COVER_DIR/diff.out | grep "Missing:" | cut -d ':' -f2 | grep -o -E '[0-9]+'` CODE_COVERAGE_UPDATE_LINES_COVER=$(( CODE_COVERAGE_UPDATE_LINES_TOTAL - miss))ficp $COVER_DIR/report.html /${your path}/res_unit/${PREFIX}update.html#程式碼行覆蓋率gocov convert $COVER_FILE | gocov-html > $COVER_DIR/line.htmlCODE_COVERAGE_LINES_COVER=`head -n 50 $COVER_DIR/coverage.xml | grep "lines-valid" | awk -F 'lines-covered''{print $2}' | awk -F ' ''{print $1}' | grep -o -E '[0-9]+'`CODE_COVERAGE_LINES_TOTAL=`head -n 50 $COVER_DIR/coverage.xml | grep "lines-valid" | awk -F 'lines-valid''{print $2}' | awk -F ' ''{print $1}' | grep -o -E '[0-9]+'`cp $COVER_DIR/line.html /${your path}/res_unit/${PREFIX}line.html#case 透過情況pass=`cat $LOG_FILE | grep -o "\--- PASS: " | wc -l`fail=`cat $LOG_FILE | grep -o "\--- FAIL: " | wc -l`echo"************************************" >> $UNIT_TEST_RESULT_FILEcat $LOG_FILE | grep "\--- FAIL: " >> $UNIT_TEST_RESULT_FILEecho"************************************" >> $UNITTEST_RESULT_FILEecho"SUCCESS:" >> $UNIT_TEST_RESULT_FILEcat $LOG_FILE | grep "\--- PASS: " >> $UNIT_TEST_RESULT_FILEecho"************************************" >> $UNIT_TEST_RESULT_FILEiconv -f UTF-8 -t gbk $UNIT_TEST_RESULT_FILE > temp.txtsed -i 's/ //g;s/---//g' temp.txtcat temp.txt > $UNIT_TEST_RESULT_FILEcp $UNIT_TEST_RESULT_FILE /${your path}/res_unit/${PREFIX}pass.txt#結果收集curl -i "http://${your server host}/unit/taskSave" -H "Content-Type:application/json" -X POST -d "{\"taskId\":\"$TaskId\", \"appName\":\"$APP_NAME\", \"branch\": \"$Branch\", \"taskRes\": \"{\\\"code_coverage_update_lines_total\\\":$CODE_COVERAGE_UPDATE_LINES_TOTAL, \\\"code_coverage_update_lines_cover\\\":$CODE_COVERAGE_UPDATE_LINES_COVER,\\\"code_coverage_lines_total\\\":$CODE_COVERAGE_LINES_TOTAL, \\\"code_coverage_lines_cover\\\":$CODE_COVERAGE_LINES_COVER, \\\"fail\\\":$fail, \\\"pass\\\":$pass}\"}"

單測任務查詢介面

PATH:/unit/taskQueryMETHOD:POSTParams:{"taskId": "123456", //可以用日期20220221102104,主要用來標識此次單測"appName":"xxxx", //應用名,根據應用名,選擇執行對應的單測指令碼}Result:如果單測執行完成,返回code="1",data是單測結果。如果單測沒完成,返回code="2",data="task ongoing",如果單測執行超過10分鐘,返回code="2",data="redis nil or delay"

單測結果儲存介面

PATH:/unit/taskSaveMETHOD:POSTParams:{"taskId": "123456", //可以用日期20220221102104,主要用來標識此次單測"appName":"xxxx", //應用名,根據應用名,選擇執行對應的單測指令碼。"taskRes":"{\"code_coverage_update_lines_total\":100,\"code_coverage_update_lines_cover\":100,\"code_coverage_lines_cover\":100,\"code_coverage_lines_total\":100,\"fail\":0,\"pass\":100}" //單測執行結果}Result:成功返回code="1"

4  實驗室配置

如1.1所述,aone實驗室只需要分發任務、輪詢任務,以及解析結果。
TASK_ID=$(date "+%Y%m%d%H%M%S")APP_NAME=`xxxx`PREFIX=$APP_NAME$TASK_IDecho$TASK_IDecho$APP_NAMEecho$PREFIXfailed="true"# 分發任務curl -i "http://${your server host}/unit/taskReceive" -X POST -H "Content-Type:application/json" -d "{\"taskId\": \"$TASK_ID\",\"appName\": \"$APP_NAME\", \"branch\": \"${branch}\", \"repo\":\"${repo}\"}"for time in 10s 30s 40s 50s 70s 100s 100s 70s 50s 40s 30s 10sdo#輪詢任務 res=$(curl "http://${your server host}/unit/taskQuery" -X POST -H "Content-Type:application/json" -d "{\"taskId\": \"$TASK_ID\",\"appName\": \"$APP_NAME\", \"branch\": \"${branch}\", \"repo\":\"${repo}\"}")echo$res code=$(echo$res | grep -o -E 'code":[0-9]' | cut -d ":" -f2) isOngoing=$(echo$res | grep -o -E 'data":[^}]*' | cut -d ":" -f2)if [ "$code" = "1" ] && [ $isOngoing != "\"ongoing\"" ] && [ $isOngoing != "null" ]then#根據res解析單元測試執行結果#略breakfi sleep $timedoneif [ "$failed" == "true" ]thenecho"Job failed"fi

5  最終結果

最終的執行結果如圖2,單元測試、行增量覆蓋率、行覆蓋率都可以點選跳轉檢視詳情。如圖3,4,5。跳轉地址的實現,是採用nginx提供的訪問靜態檔案功能。只需要在nginx的配置檔案中,增加配置
location ^~ /res_unit {root /${your path};}
這樣,如果想訪問a.html檔案,只需要將其放在/${your path}/res_unit/a.html。就可以透過連結https://${your server host}/res_unit/a.html訪問到。
圖2 aone單測執行示例
圖3 case透過情況
圖4 行增量覆蓋率
圖5 行覆蓋率

三  其他

招聘高德共享出行技術質量團隊求賢若渴(北京崗),誠招Java開發P6&P7、測試開發工程師P6&P7,有意向歡迎聯絡[email protected]
參考連結:
[1]https://github.com/axw/gocov
[2]https://github.com/AlekSi/gocov-xml
[3]https://github.com/Bachmann1234/diff_cover

尋找天貓精靈首席評測官,贏戴森吸塵器等千元好禮
來開發平臺建立技能,曬出技能開發文件。成為天貓精靈最耀眼的評測之星,享受天貓精靈大禮包,並受邀成為大會特邀嘉賓,對話產品leader。還有機會贏得戴森吸塵器、倍輕鬆AI眼精靈、千元天貓超市購物券等超值禮品!還有官方證書頒發~是時候展現真正的“技術”了,點選閱讀原文馬上參加!

相關文章