当前位置: 首页 > 科技观察

Go应用单元测试实践

时间:2023-03-15 14:28:24 科技观察

一、背景高德运营的大部分应用都是基于Go开发的。我们希望在预集成环境中,在代码开发部署时,能够自动触发单元测试和接口自动化测试,并生成覆盖率报告。参考了很多go单元测试的文章,有的缺少行增量覆盖,有的缺少case运行结果/case运行日志。本文旨在搭建一个运行稳定、维护成本低的单元测试/集成测试环境。2.单元测试1.单测运行概览图1单测运行流程图aone作为阿里巴巴集团的数字化研发协同平台,提供各类一体化测试实验室,可运行自定义脚本。如图1所示,是单元测试操作的流程图。单元测试由aone实验室脚本触发。Java服务收到单元测试任务后,调用单元测试脚本并执行。最后由aone实验室轮询运行结果。单元测试不直接在单元测试实验室脚本中运行的主要原因有两个。一是单机测试的运行依赖于GO环境和生成覆盖率文件需要的一些第三方工具。目前aonelaboratory不支持自定义镜像接入,每次运行都需要安装环境,比运行单个测试耗时要多很多。二是每个应用的单测运行命令可能不同。一旦申请量大,如果需要调整单测脚本,变更成本比较高。因此,启动一个JAVA服务(可以复用已有的服务来降低成本),并打包在这个服务上运行单元测试所需的脚本和环境。aone上的实验室脚本只进行单次测试任务下发、轮询和运行结果展示。具体流程如下:开发在预集成环境提交代码并部署完成后,流程自动运行单测实验室。对于单元测试实验室中的脚本,首先调用任务下发接口/unit/taskReceive,然后Java服务会调用相应的单元测试脚本。由于单元测试脚本运行时间较长,/unit/taskReceive接口会超时。单元测试脚本运行时,单元测试实验室脚本会一直调用/unit/taskQuery接口查询单元测试任务的状态,直到返回正确的结果。当单个测试脚本完成后,会回调任务完成接口/unit/taskSave接口保存结果。这样,当单元测试lab脚本调用/unit/taskQuery接口进行查询时,就会返回单元测试的结果。单测实验室脚本根据任务返回的结果分析并展示单测结果。2、环境搭建将需要的环境打包到Java服务的docker中:gotest在golang中安装go时需要运行gotest,所以需要在环境中安装go。安装完成后,配置环境变量和代理。wgethttps://golang.google.cn/dl/go1.17.8.linux-amd64.tar.gztar-zxvfgo1.17.8.linux-amd64.tar.gz-C/usr/local/mkdir-p/${你的路径目录}/gopathecho-e"exportPATH=\"$PATH:/usr/local/go/bin:/${你的路径目录}/gopath/bin\"\nexportGOPATH=\"/${你的go路径dir}/gopath\"\nexportGOPROXY=\"${go代理地址},direct\"">>/etc/profilesource/etc/profile代码覆盖插件安装使用一些开源工具生成单test覆盖文件转换为xml/html格式的覆盖文件。主要用到gocov-html、gocov、gocov-xml。请参阅地址[1][2]。gogetgithub.com/matm/gocov-htmlgogetgithub.com/axw/gocov/...gogetgithub.com/AlekSi/gocov-xml行增量覆盖工具安装使用diff-cover[3]生成Row增量覆盖。diff-cover依赖python3,安装python3可能需要先安装gcc、automake、autoconf、libtool、make、zlib、zlib-developenssl。yum-y安装gccautomakeautoconflibtoolmakezlibzlib-developensslopenssl-develwgethttps://www.python.org/ftp/python/3.8.1/Python-3.8.1.tgztar-zxvfPython-3.8.1。tgz&&cdPython-3.8.1&&./configure&&make&&makeinstallpip3installdiff-cover-ihttps://mirrors.aliyun.com/pypi/simplgitinstall&configure运行单元测试时,依赖开发的代码.下载代码需要配置一个有代码权限的gitssh公钥和私钥。yum-ygitname=`gitconfiguser.name`if[-z"$name"]thengitconfig--globaluser.name"xxx"gitconfig--globaluser.email"xxxx@xxxx.xxxx.com"mkdir-p~/.sshcp${你的id_rsa}~/.ssh/fi3。Java服务实现单测任务下发接口Path:/unit/taskReceiveMethod:POSTParams:{"taskId":"123456",//可以使用日期20220221102104,主要用于识别单测"appName":"ApplicationA",//应用名称,根据应用名称,选择运行对应的单测脚本。比如应用A会运行applicationA.sh"branch":"releases/test-branch-code",//需要运行单机测试的分支名称"repo":"git@xxxxx.git"//应用AAddress的代码,下载代码后可以跑单机测试}结果:什么都可以返回,反正都会超时。具体实现逻辑:将本次单测任务记录在redis中,key:"${appName}${taskId}-unit",value:"ongoing"。为了查询/unit/taskQuery,从而知道单元测试还在运行。根据appName参数,选择执行${appName}.sh脚本。如果脚本不存在,去阿里云对象存储服务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/${你的路径}/res_unit#下载代码cd$DIR/$APP_NAME/$TaskIdgitclone-b$Branch$Repo#rununittestcd./$APP_NAMECONF_DIR=$DIR/$APP_NAME/$TaskId/$APP_NAME/confgotest./...-timeout3m-v-gcflags=-l-cover=true-coverprofile=$COVER_FILE-mod=vendor-args--confDir=$CONF_DIR>>$LOG_FILE#行增量覆盖gocovconvert$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/差异输出|grep“总计:”|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-未命中))ficp$COVER_DIR/report.html/${你的路径}/res_unit/${PREFIX}update.html#代码行覆盖率gocovconvert$COVER_FILE|gocov-html>$COVER_DIR/line.htmlCODE_COVERAGE_LINES_COVER=`head-n50$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-n50$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/${你的路径}/res_unit/${PREFIX}line.html#case通过情况pass=`cat$LOG_FILE|grep-o"\---通过:"|wc-l`fail=`cat$LOG_FILE|grep-o"\---失败:"|wc-l`echo“***************************************”>>$UNIT_TEST_RESULT_FILEcat$LOG_FILE|grep"\---失败:">>$UNIT_TEST_RESULT_FILEecho"***************************************">>$UNITTEST_RESULT_FILEecho"成功:">>$UNIT_TEST_RESULT_FILEcat$LOG_FILE|grep"\---PASS:">>$UNIT_TEST_RESULT_FILEecho"******************************************">>$UNIT_TEST_RESULT_FILEiconv-fUTF-8-tgbk$UNIT_TEST_RESULT_FILE>temp.txtsed-i's///g;s/---//g'temp.txtcattemp.txt>$UNIT_TEST_RESULT_FILEcp$UNIT_TEST_RESULT_FILE/${你的路径}/res_unit/${PREFIX}pass.txt#结果收集curl-i"http://${你的服务器主机}/unit/taskSave"-H"Content-Type:application/json"-XPOST-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",//应用名称,根据应用名称,选择运行对应的单测脚本}结果:如果单测运行完成,返回code="1",数据为单测result如果单测没有完成,返回code="2",data="taskongoing",如果单测运行超过10分钟,返回code="2",data="redisnilordelay"单测结果保存接口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,\"失败\":0,\"通过\":100}"//单次测试运行结果}结果:成功返回code="1"4.实验室配置如1.1所述,一个实验室只需要分发任务,轮询任务,分析结果即可。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://${你的服务器主机}/unit/taskReceive"-XPOST-H"Content-Type:application/json"-d"{\"taskId\":\"$TASK_ID\",\"appName\":\"$APP_NAME\",\"branch\":\"${branch}\",\"repo\":\"${repo}\"}"10s30s40s50s70s100s100s70s50s40s30s10sdo#Polltaskres=$(curl"http://${yourserverhost}/unit/taskQuery"-XPOST-H"Content-Type:application/json"-d"{\"taskId\":\"$TASK_ID\",\"appName\":\"$APP_NAME\",\"branch\":\"${branch}\",\"repo\":\"${repo}\"}")echo$rescode=$(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解析单元测试运行结果#略断fisleep$timedoneif["$failed"=="true"]thenecho"Jobfailed"fi5.最终结果最终运行结果如图2所示,单元测试、行增量覆盖、行覆盖可以点击查看详情如图3、4、5.跳转地址的实现是利用nginx提供的访问静态文件的功能,你只需要在nginx的配置文件中添加配置location^~/res_unit{root/${你的路径};}在这方式,如果你想访问a.html文件,只需将它放在/${你的路径}/res_unit/a.html。它可以通过链接访问https://${你的服务器主机}/res_unit/a.html.图2Aone单机试运行示例图3Case通过状态图4线路增量覆盖图5线路覆盖3.其他招聘高德共享出行技术质量团队求贤若渴(北京帮),诚招Java开发P6&P7,测试开发工程师P6&P7.参考链接:[1]https://github.com/axw/gocov[2]https://github.com/AlekSi/gocov-xml[3]https://github.com/Bachmann1234/diff_cover