Go应用单元测试实践

2022-04-08 00:00:00 代码 增量 运行 脚本 覆盖率
> /etc/profile\nsource /etc/profile"]]],["p",{"list":{"listId":"yi5fooug2a","level":0,"isOrdered":false,"isTaskList":false,"listStyleType":"SCIR_ECIR_SREC","symbolStyle":{},"listStyle":{"format":"bullet","text":"●","align":"left"},"hideSymbol":false},"ind":{"left":0}},["span",{"data-type":"text"},["span",{"bold":true,"data-type":"leaf"},"代码覆盖率插件安装"]]],["p",{"ind":{"firstLine":32}},["span",{"data-type":"text"},["span",{"data-type":"leaf"},"运用一些开源工具,将单测生成的覆盖文件转换成xml/html格式的覆盖率文件。主要用到gocov-html,gocov,gocov-xml。参考地址:"]],["a",{"href":"https://github.com/axw/gocov"},["span",{"data-type":"text"},["span",{"data-type":"leaf"},"https://github.com/axw/gocov"]]],["span",{"data-type":"text"},["span",{"data-type":"leaf"},","]],["a",{"href":"https://github.com/AlekSi/gocov-xml"},["span",{"data-type":"text"},["span",{"data-type":"leaf"},"https://github.com/AlekSi/gocov-xml"]]],["span",{"data-type":"text"},["span",{"data-type":"leaf"},"。"]]],["code",{"syntax":"text/x-sh","theme":"default","height":null,"id":"m5f4lf"},["span",{"data-type":"text"},["span",{"data-type":"leaf"},"go get github.com/matm/gocov-html\ngo get github.com/axw/gocov/... \ngo get github.com/AlekSi/gocov-xml"]]],["p",{"list":{"listId":"m6ra4b3xx","level":0,"isOrdered":false,"isTaskList":false,"listStyleType":"SCIR_ECIR_SREC","symbolStyle":{},"listStyle":{"format":"bullet","text":"●","align":"left"},"hideSymbol":false},"ind":{"left":0}},["span",{"data-type":"text"},["span",{"bold":true,"data-type":"leaf"},"行增量覆盖率工具安装"]]],["p",{"ind":{"firstLine":32}},["span",{"data-type":"text"},["span",{"data-type":"leaf"},"利用diff-cover("]],["a",{"href":"https://github.com/Bachmann1234/diff_cover"},["span",{"data-type":"text"},["span",{"data-type":"leaf"},"https://github.com/Bachmann1234/diff_cover"]]],["span",{"data-type":"text"},["span",{"data-type":"leaf"},"),生成行增量覆盖率。diff-cover依赖python3,python3的安装可能需要先装好gcc,automake,autoconf,libtool,make,zlib,zlib-devel openssl。"]]],["code",{"syntax":"text/x-sh","theme":"default","height":null,"id":"438ulg"},["span",{"data-type":"text"},["span",{"data-type":"leaf"},"yum -y install gcc automake autoconf libtool make zlib zlib-devel openssl openssl-devel\nwget https://www.python.org/ftp/python/3.8.1/Python-3.8.1.tgz\ntar -zxvf Python-3.8.1.tgz && cd Python-3.8.1 && ./configure && make && make install \npip3 install diff-cover -i https://mirrors.aliyun.com/pypi/simpl"]]],["p",{"list":{"listId":"uc1okiy1kvd","level":0,"isOrdered":false,"isTaskList":false,"listStyleType":"SCIR_ECIR_SREC","symbolStyle":{"bold":true},"listStyle":{"format":"bullet","text":"●","align":"left"},"hideSymbol":false},"ind":{"left":0}},["span",{"data-type":"text"},["span",{"bold":true,"data-type":"leaf"},"git安装&配置"]]],["p",{"ind":{"left":0,"firstLine":29.333333333333336}},["span",{"data-type":"text"},["span",{"data-type":"leaf"},"运行单元测试时,依赖开发的代码。需要配置好一个有代码权限的git ssh公钥和私钥,用来下载代码。"]]],["code",{"syntax":"text/x-sh","theme":"default","height":null,"id":"qys9hz"},["span",{"data-type":"text"},["span",{"data-type":"leaf"},"yum -y git \nname=" git="git" config="config" user="user" z="z" quot="quot" nthen="nthen" global="global" mkdir="mkdir" p="p" ssh="ssh" cp="cp" your="your" id_rsa="id_rsa" java="java" n="n" a="a" storage="storage" service="service" etc="$1\nBranch=$2\nTaskId=$3\nRepo=$4\nDIR=" pwd="$APP_NAME$TaskId\n\n#生成覆盖率文件的文件夹\nmkdir"" dir="$DIR/$APP_NAME/$TaskId/cover/core.cover\nLOG_FILE=$DIR/$APP_NAME/$TaskId/cover/log.txt\nCOVER_DIR=$DIR/$APP_NAME/$TaskId/cover\nUNIT_TEST_RESULT_FILE=$DIR/$APP_NAME/$TaskId/cover/unit_pass.txt\n\n#存放覆盖率详情html文件的文件夹\nmkdir" path="path" clone="clone" b="b" branch="branch" repo="repo" app_name="$DIR/$APP_NAME/$TaskId/$APP_NAME/conf\ngo" test="test" timeout="timeout" m="m" v="v" gcflags="-l" cover="true" coverprofile="$COVER_FILE" mod="vendor" args="args" confdir="$CONF_DIR" log_file="log_file" convert="convert" cover_file="cover_file" gocov-xml="gocov-xml" cover_dir="cover_dir" compare-branch="origin/master" html-report="html-report" nif="nif" echo="echo" 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="$((" gocov-html="gocov-html" ncode_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]+'" ncp="ncp" npass="cat $LOG_FILE | grep -o \"\\--- PASS: \" | wc -l" nfail="cat $LOG_FILE | grep -o \"\\--- FAIL: \" | wc -l" necho="necho" unit_test_result_file="unit_test_result_file" grep="grep" fail:="fail:" unittest_result_file="unittest_result_file" pass:="pass:" f="f" utf-8="utf-8" t="t" gbk="gbk" temp="temp" i="i" s="s" g="g" server="server" host="host" h="h" x="x" post="post" d="d" nil="nil" or="or" delay="\"${appName}${taskId}-unit\"对应的值即可。"]]],["h3",{"spacing":{"before":12,"after":12,"line":0.8529411764705882}},["span",{"data-type":"text"},["span",{"bold":true,"sz":14,"szUnit":"pt","data-type":"leaf"},"1.3.3" nprefix="$APP_NAME$TASK_ID\n\necho" task_id="task_id" prefix="\"true\"" ncurl="ncurl" time="time" in="in" res="$(curl" code="#" o="o" e="e" cut="cut" f2="f2" tisongoing="$(echo" data="data" if="if" amp="amp" isongoing="isongoing" then="then" break="break" fi="fi" sleep="sleep" failed="failed" res_unit="res_unit" root="root" aone="aone" case="case">


一  背景


高德打车运营的应用大多基于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 "xxxx@xxxx.xxxx.com"  mkdir -p ~/.ssh  cp ${your id_rsa} ~/.ssh/fi

3  Java服务实现


单测任务下发接口

Path:/unit/taskReceive
Method:POST
Params:{ "taskId": "123456", //可以用日期20220221102104,主要用来标识此次单测 "appName":"应用A", //应用名,根据应用名,选择运行对应的单测脚本。比如应用A就会运行应用A.sh "branch":"releases/test-branch-code", //需要运行单测的分支名 "repo":"git@xxxxx.git" //应用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/profile
APP_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" ]then echo "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/taskQuery
METHOD:POST
Params:{ "taskId": "123456", //可以用日期20220221102104,主要用来标识此次单测 "appName":"xxxx", //应用名,根据应用名,选择运行对应的单测脚本}
Result:如果单测运行完成,返回code="1",data是单测结果。如果单测没完成,返回code="2",data="task ongoing",如果单测运行超过10分钟,返回code="2",data="redis nil or delay"


单测结果保存接口

PATH:/unit/taskSave
METHOD:POST
Params:{ "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_ID
echo $TASK_IDecho $APP_NAMEecho $PREFIX
failed="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解析单元测试运行结果 #略 break fi sleep $timedone
if [ "$failed" == "true" ]then echo "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 行覆盖率



参考链接:

[1]https://github.com/axw/gocov

[2]https://github.com/AlekSi/gocov-xml

[3]https://github.com/Bachmann1234/diff_cover

文章来源:公众号-阿里技术


相关文章