一次單元測試優(yōu)化的過程總結(jié)
前言
淘寶原用戶增長團(tuán)隊(duì)(現(xiàn)用戶運(yùn)營平臺(tái)團(tuán)隊(duì))是比較早踐行單測增量覆蓋率的團(tuán)隊(duì),堅(jiān)持了近兩年下來,我們積累了數(shù)千個(gè)test case,在開發(fā)新功能、修改原功能的過程中幫助我們發(fā)現(xiàn)了許多問題,顯著地提升了代碼質(zhì)量、減少線上故障。在這里鄭重地向大家推薦,單測是值得認(rèn)真做的,開頭是痛苦的,但是積累一段時(shí)間后,量變就會(huì)帶來質(zhì)變。
言歸正傳,接下來談一談最近在實(shí)踐單測過程中遇到的一個(gè)問題。在研發(fā)協(xié)同平臺(tái)aone(下文簡稱aone)的發(fā)布流水線中,我們針對(duì)單元測試設(shè)置了增量代碼覆蓋率85%和test case 100%通過的流程卡點(diǎn),在每次發(fā)布前,要保證test case完全通過才能提交工單。我們遇到了因并發(fā)導(dǎo)致的test case失敗,調(diào)整并發(fā)度導(dǎo)致的單測時(shí)間過長,但又影響研發(fā)效能的問題。最終在并發(fā)度和成功率之間找到了一個(gè)平衡點(diǎn),解決了單測流程降低研發(fā)效率的問題。
單側(cè)流水線配置
在單測流程中呢,我們主要用到了JUnit、JaCoCo和Surefire三套工具,通過aone提供的容器自動(dòng)化運(yùn)行單元測試,搜集測試報(bào)告。下面簡單介紹一下這三個(gè)工具。
? JUnit
java界最大名鼎鼎的單元測試框架,無須多言,會(huì)java的應(yīng)該都知道。
? JaCoCo
EclEmma團(tuán)隊(duì)開發(fā)的開源代碼覆蓋率統(tǒng)計(jì)工具,也是java業(yè)內(nèi)最主流的代碼覆蓋率統(tǒng)計(jì)工具。增量代碼覆蓋率就是通過該工具進(jìn)行統(tǒng)計(jì)的,全量、增量、按類、包統(tǒng)計(jì)都支持,非常靈活。
? Maven Surefire Plugin
surefire是maven的一個(gè)插件,在maven生命周期的test階段執(zhí)行單元測試用例。運(yùn)行完成后還會(huì)生成測試報(bào)告,方便用戶查看單測情況。
我們利用三種工具,加上aone提供的容器和流水線配置能力,完成了自動(dòng)化單測的流程和發(fā)布卡點(diǎn)校驗(yàn)。
單元實(shí)踐過程
? 兩個(gè)階段
- 積累test case時(shí)期
在剛剛開始單測時(shí),大家新增的代碼都相對(duì)比較獨(dú)立,隨著業(yè)務(wù)的發(fā)展、工作職責(zé)的調(diào)整,單測會(huì)不斷變復(fù)雜,不同的service之間互相交織、單測的維護(hù)、運(yùn)行成本都會(huì)增加。我們?cè)谶@個(gè)階段遇到了一個(gè)比較棘手的問題。日常開發(fā)過程中,單測都是以類為粒度在本地跑的,都能通過后再去流水線驗(yàn)證,一旦提交到流水線,就會(huì)遇到個(gè)別case失敗的問題,一開始排查起來完全沒有思路,test case的失敗可以說是隨機(jī)的,任何一個(gè)類的任何一個(gè)用例都有可能失敗。
經(jīng)過分析和排查,得出結(jié)論是并發(fā)導(dǎo)致的,于是我們限制了并發(fā),做了如下配置,確實(shí)解決了這個(gè)問題。
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.16</version>
<configuration>
<reuseForks>false</reuseForks>
<forkCount>1</forkCount>
</configuration>
</plugin>
大家可以留意一下reuseForks和forkCount參數(shù),這時(shí)候我們還沒有深究兩個(gè)配置的含義,只是簡單的限制了并發(fā),這也為后續(xù)的故事埋下了伏筆。
- test case達(dá)到一定規(guī)模時(shí)期
在完成了test case的初始積累以后,新的問題又隨之而來。因?yàn)闆]有并發(fā),test case又很多,所以每次單測運(yùn)行時(shí)長長達(dá)50分鐘。也嚴(yán)重影響了大家的研發(fā)效率。在分秒必爭的發(fā)布窗口期,經(jīng)常會(huì)出現(xiàn)大家等著單測跑完提交發(fā)布單的情況。
? 問題
看了上述兩個(gè)不同階段反映的問題,本質(zhì)上就是成功率和實(shí)效性的trade off問題,如何能提高并發(fā)、提升運(yùn)行速度的同時(shí)保障成功率,這就是我們需要解決的最終命題。
? 原理和解決方案
上文提到了reuseForks和forkCount參數(shù),這些都是maven-surefire-plugin提供的配置項(xiàng),把surefire插件研究清楚了,應(yīng)該就能解決如何兼顧速度和實(shí)效性的問題。
- Surefire配置詳解
parallel
jvm內(nèi)并行執(zhí)行
通過parallel參數(shù)開啟,可選為methods,classes,both,suites等
其他參數(shù)
- useUnlimitedThreads,不限制線程數(shù)
- threadCount,線程數(shù)
- perCoreThreadCount,每核(默認(rèn)true,和threadCount組合使用)
- parallelTestsTimeoutInSeconds,timeout時(shí)間
注
- 設(shè)置了parallel后,useUnlimitedThreads或者threadCount必須設(shè)置一個(gè),不然會(huì)報(bào)錯(cuò)
- parallel級(jí)別還有suitesAndClasses等更復(fù)雜的配置項(xiàng),本文不多探討
參數(shù)示例如下,代表methods級(jí)別并發(fā),10條線程執(zhí)行。
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M7</version>
<configuration>
<parallel>methods</parallel>
<threadCount>10</threadCount>
</configuration>
</plugin>
</plugins>
fork
- 多jvm并行執(zhí)行
- forkCount 最多同時(shí)生成的JVM個(gè)數(shù),特殊語法是nC,代表n倍的CPU核數(shù),2.5C在4核機(jī)器上就是10的意思。
- reuseForks 是否重復(fù)使用fork出的JVM,true代表一個(gè)測試類運(yùn)行完后,進(jìn)程繼續(xù)處理下一個(gè),false代表一個(gè)類運(yùn)行完了JVM銷毀,重新生成新的JVM
默認(rèn)配置 forkCount=1/reuseFork=true,forkCount設(shè)置為0會(huì)被自動(dòng)替換為1
parallel和fork
parallel和fork組合后,就可以有更好的并發(fā)效率,也會(huì)帶來更大的沖突可能。
- 并發(fā)導(dǎo)致case失敗原因
surefire的文檔原文如下,
簡單說來,就是因?yàn)镴Unit的實(shí)現(xiàn)機(jī)制,對(duì)于JVM內(nèi)的線程并發(fā),會(huì)出現(xiàn)一些race condition或者其他難以復(fù)現(xiàn)的問題;對(duì)于forkCount大于1且開啟復(fù)用的情況,因?yàn)闇y試類是在復(fù)用的JVM內(nèi),也會(huì)因?yàn)橄嗤脑虍a(chǎn)生并發(fā)問題導(dǎo)致測試失敗。
- 結(jié)果和建議
在徹底搞清楚surefire的配置原理后,我們回到問題來。經(jīng)過各種排列組合的嘗試,我們得出了比較合適的配置,reuseForks=true/ forkCount=2C,最終效果是每次運(yùn)行時(shí)間在10分鐘左右,出錯(cuò)概率較低,通過重跑也能解決。
小tip
mvn默認(rèn)是按模塊串行的,可開啟并行提高整體速度(例:mvn -T 1C clean test),但是在我們的場景下,2000多個(gè)test case有1800個(gè)都在一個(gè)模塊里,所以開啟并行的效果不大。
其實(shí)這個(gè)問題沒有最優(yōu)組合,只有最合適的組合。在優(yōu)化了這個(gè)單測耗時(shí)最久的應(yīng)用后,我們又分析了其他幾個(gè)應(yīng)用,有的應(yīng)用test case不多,單測運(yùn)行時(shí)長不長,就沒有必要開啟并發(fā),優(yōu)先保證成功率即可;有的應(yīng)用test case直接相互干擾較小,并發(fā)度可以調(diào)整得更高……
總的來說,在弄明白了原理之后,還需要具體情況具體分析,“紙上得來終覺淺,絕知此事要躬行”,大家可以分析一下自己應(yīng)用的情況,結(jié)合surefire的并發(fā)機(jī)制進(jìn)行實(shí)踐,相信測過幾次以后就能找到最合適的配置組合。
單元實(shí)踐過程
在整個(gè)過程中,筆者還留有兩個(gè)想法:
- 有沒有辦法通過提高單測代碼質(zhì)量來避免或者降低因?yàn)椴l(fā)引起的失敗?一些思路是通過suite分組,將可能沖突的類分開跑,這樣的做法可能會(huì)極大的提高單測開發(fā)成本,投入產(chǎn)出比不高。
- test case通過率可以不用嚴(yán)格卡100%,設(shè)定到99.5%都能顯著的提升效率,因?yàn)槊看问〉膖est case是不固定的,所以偶發(fā)的個(gè)別問題不影響整體的回歸。
在實(shí)踐卓越工程的過程中,筆者深切的感受到縱觀整個(gè)軟件研發(fā)的生命周期,有很多值得研究和切入的點(diǎn),一些微小的改動(dòng),都能有效地提升研發(fā)效能和交付質(zhì)量。在當(dāng)前的環(huán)境下,業(yè)務(wù)競爭日趨激烈,所謂開源節(jié)流,“開源”難,重心就會(huì)偏向“節(jié)流”,降本增效一定會(huì)是下一個(gè)階段的重點(diǎn)。而且對(duì)于技術(shù)人來說,效率一定是永遠(yuǎn)的追求。其實(shí)提升性能、效率往往不是特別高大上的事情,希望大家能在日常繁重的工作之余,有點(diǎn)時(shí)間做些有趣的研究,享受技術(shù)帶來的快樂!