Mock框架的三次迭代,讓你的單元測試更高效
如何定義單元
對于單元測試中的單元,不同的人有不同的看法:可以理解為一個方法,可以理解為一個完整的接口實(shí)現(xiàn),也可以理解為一個完整的功能模塊或者是多個功能模塊的一個耦合。
根據(jù)以往的單元測試經(jīng)驗(yàn),在設(shè)計(jì)單元測試用例時,當(dāng)針對方法級別展開單元測試時,重點(diǎn)關(guān)注的是方法的底層邏輯;當(dāng)針對的是模塊時,針對的是實(shí)際的業(yè)務(wù)邏輯實(shí)現(xiàn);當(dāng)針對整合后的模塊進(jìn)行測試時,一般稱之為集成測試。
不管是單元測試還是集成測試,都可以統(tǒng)一的理解為單元測試。因?yàn)樗麄兊谋举|(zhì)都是對方法或接口的一種測試形式,只是所處的階段不一樣罷了。
1. 集成測試應(yīng)該由誰編寫
在我們的實(shí)際工作中,研發(fā)人員在提交代碼之前,會設(shè)計(jì)一些“冒煙測試”級別集成測試用例。等到整個功能開發(fā)完成后,測試人員會根據(jù)業(yè)務(wù)需求和設(shè)計(jì)的測試用例,來進(jìn)行整體的集成測試用例的編寫、執(zhí)行、失敗用例分析,以及代碼的調(diào)式和問題代碼的定位等工作。
2. 集成測試用例
業(yè)務(wù)相關(guān)的測試主要是通過spring-test來進(jìn)行集成測試,基本的測試結(jié)構(gòu)為先定義一個基類用來初始化被測試類。
測試基類定義結(jié)構(gòu)如下:
- @RunWith(SpringJUnit4ClassRunner.class)
- ContextConfiguration(locations = {"classpath:./spring/applicationContext.xml"})
- @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
- public class BaseSpringJunitTest {
- @Autowired
- protected BusinessRelatedServiceImpl businessRelatedService;
- }
業(yè)務(wù)相關(guān)的測試類定義如下格式:
- public class BusinessRelatedServiceImplDomainTest extends BaseSpringJunitTest {
- @Test
- public void testScenario1 (){
- new Thread(new DOSAutoTest("testScenario1")).start();
- Thread.sleep(1000*60*1);
- String requestJson=""//測試入?yún)?
- RequestPojo request=( RequestPojo )JSONUtils.jsonToBean(requestJson,RequestPojo .class);
- ResponsePojo response= businessRelatedService.businessRelatedMethod(ResponsePojo );
- //業(yè)務(wù)相關(guān)的assert區(qū)域
- }
- }
3. 如何解決下游系統(tǒng)依賴
businessRelatedMethod方法在處理業(yè)務(wù)邏輯的過程中需要調(diào)用下游JSF(Jingdong Service Framework,完全自主研發(fā)的高性能RPC服務(wù)框架)提供的訂單接口(OrderverExportService),并根據(jù)入?yún)⒅械挠唵尉幪柅@取訂單的詳細(xì)信息(ResultPojo getOrderInfoById (long orderId))。
那么如何獲取下游JSF接口的返回正確數(shù)據(jù)就變成了一個比較重要的問題。如果是在功能測試或者聯(lián)調(diào)測試階段,可以由下游測試人員來提供數(shù)據(jù)。不過這樣溝通和測試成本較高,無法滿足業(yè)務(wù)快速上線和變化的要求,尤其在集成測試階段這個問題就變得尤為明顯,因?yàn)橄掠螖?shù)據(jù)對于上游來說是不可控的。這樣mock下游數(shù)據(jù)就變得尤為緊急和重要。
4. Mock框架的選擇
在整個java生態(tài)圈中,支持mock的開源框架還是比較多的,比如常用的mockito、powermock、easymock和jmockit等開源框架。這些框架在mock方面都具有比較強(qiáng)大的功能與比較廣泛的使用量。但是這些框架都具有一個相同的缺點(diǎn),那就是需要或多或少的編碼工作來mock所需要的接口返回?cái)?shù)據(jù)。
在設(shè)計(jì)mock框架的時候,我們考慮到盡量讓寫單元測試的人員或研發(fā)人員少編碼或不編碼,來獲取不同的業(yè)務(wù)場景所需要的測試數(shù)據(jù)。
Mock框架 ***版
該版本的mock框架的整體思想為:結(jié)合JSF的特性,Override所有下游接口的方法,然后將實(shí)現(xiàn)下游接口的應(yīng)用部署到測試環(huán)境,發(fā)布一個有別與真實(shí)下游接口的服務(wù),在接口調(diào)用的時候,通過不同的JSF接口別名來進(jìn)行區(qū)分。Mock的數(shù)據(jù)存儲在數(shù)據(jù)庫中。
該框架類調(diào)用關(guān)系:
Mock接口的具體實(shí)現(xiàn):
- public class OrderverExportServiceImp extends OrderverExportServiceAdapter {
- @Resource
- private OrderverMapper orderverMapper;
- @Override
- public ResultPojo getOrderInfoById (long orderId) {
- OrderverPojo orderverMock=orderverMapper.getOrderId(new Long(orderId).toString());
- ResultPojo result=new ResultPojo ();
- result.setFiled1(null);
- result.setFiled1(0);
- result.setFiled2(null);
- result.setFiled3(null);
- result.setResult(true);
- …//mock需要的數(shù)據(jù)
- result.setReturnObject(orderver);
- return result;
- }
- }
Mock服務(wù)發(fā)布完后的效果:
在集成測試階段,只需要修改該接口的JSF別名,就可以實(shí)現(xiàn)該接口的mock調(diào)用。
- <jsf:consumer id="orderverExportServiceJsf" interface="xxx.xxx.xxx.xxx.xxx.OrderverExportService"
- protocol="jsf" timeout="${timeout}"
- alias="${alias}" retries="2" serialization="hessian">
- </jsf:consumer>
- alias=orderver_mock
該框架的優(yōu)缺點(diǎn)
優(yōu)點(diǎn):
- 做集成測試用例設(shè)計(jì)時,不用編寫代碼,只需要維護(hù)測試場景所需要的返回?cái)?shù)據(jù);
- 該框架不僅可以用在集成測試中,在下游接口無變更的前提下,同時還可以用在后續(xù)系統(tǒng)測試與聯(lián)調(diào)測試階段。
缺點(diǎn):
- mock服務(wù)的發(fā)布依賴于服務(wù)器與數(shù)據(jù)庫,當(dāng)依賴的服務(wù)器或數(shù)據(jù)庫出現(xiàn)跌機(jī)情況時,該mock服務(wù)不用;
- 該框架的維護(hù)成本比較大,當(dāng)下游依賴的接口較多時,所有的服務(wù)包含的方法均需要進(jìn)行override;
- 當(dāng)下游的接口定義發(fā)生變化時比如新增接口方法,該mock服務(wù)需要重新override該新增的方法并且需要重新打包部署;
- 下游接口方法的數(shù)據(jù)結(jié)構(gòu)發(fā)生變化時,存儲數(shù)據(jù)的數(shù)據(jù)表結(jié)構(gòu)需要做相應(yīng)的調(diào)整,對于業(yè)務(wù)變化較快的系統(tǒng),這種類型的改動頻率還是較高。
Mock框架 第二版
為了解決上述mock框架依賴服務(wù)器與數(shù)據(jù)庫的問題,我們又做了第二次嘗試。將mock框架設(shè)計(jì)為jar包的形式,提供給程序來調(diào)用。在下游接口的實(shí)現(xiàn)方式上第二版與***版保持不變,同時業(yè)務(wù)數(shù)據(jù)不放數(shù)據(jù)庫,而是將業(yè)務(wù)數(shù)據(jù)放到文件中。變化的點(diǎn)為接口調(diào)用上需要將對應(yīng)的jsf:comsumer節(jié)點(diǎn)替換為對應(yīng)的實(shí)際mock的實(shí)現(xiàn)類。
Mock接口的實(shí)現(xiàn):
- @Service("orderverExportService")
- public class OrderverExportServiceMock extends OrderverExportServiceAdapter {
- @Override
- public ResultPojo getOrderInfoById(long orderId) {
- ResultPojo result=new ResultPojo ();
- result.setFiled1(null);
- result.setFiled1(0);
- result.setFiled2(null);
- result.setFiled3(null);
- result.setResult(true);
- …//mock需要的數(shù)據(jù)
- result.setReturnObject(orderver);
- return result;
- }
- }
Mock接口調(diào)用配置:
- <!--<jsf:consumer id="orderverExportServiceJsf" interface="xxx.xxx.xxx.xxx.xxx.OrderverExportService" protocol="jsf" timeout="${timeout}"alias="${alias}" retries="2" serialization="hessian">
- </jsf:consumer>-->
- <bean id="orderverExportServiceJsf" class="xxx.xxx.xxx.xxx.xxx.OrderverExportServiceMock"></bean>
該框架的優(yōu)缺點(diǎn)
優(yōu)點(diǎn):
- 做集成測試用例設(shè)計(jì)時,不用編寫代碼,只需要維護(hù)測試場景所需要的返回?cái)?shù)據(jù);
- 相比較***個版本,該版本在執(zhí)行效率上有了較大的提升,因?yàn)閙ock類的加載是走的本地Spring配置文件,同時數(shù)據(jù)加載也是走的本地文件;
- 無需再依賴于服務(wù)器部署和數(shù)據(jù)庫依賴。
缺點(diǎn):
- 該框架的維護(hù)成本比較大,當(dāng)下游依賴的接口較多時,所有的服務(wù)包含的方法均需要進(jìn)行override;
- 當(dāng)下游的接口定義發(fā)生變化時比如新增接口方法,該mock服務(wù)需要重新override該新增的方法并且需要重新打包,然后上傳到maven倉庫;
- 下游接口方法的數(shù)據(jù)結(jié)構(gòu)發(fā)生變化時,對于業(yè)務(wù)變化較快的系統(tǒng),這種類型的改動頻率還是較高。
Mock框架 第三版
隨著需要mock的接口變的越來越龐大,以上兩種mock框架的實(shí)現(xiàn)的缺點(diǎn)就變的越來越突出。該框架可以說從根本上解決了上述框架實(shí)現(xiàn)的問題。因?yàn)樵摽蚣艹浞掷昧薐DK的動態(tài)代理,反射機(jī)制以及JSF提供的高級特性來實(shí)現(xiàn)我們的mock框架??蚣芫S護(hù)任務(wù)可以做到無需做更多的針對接口的編碼任務(wù)。測試人員只需要將重點(diǎn)放在測試數(shù)據(jù)的準(zhǔn)備上。
框架整體調(diào)用時序圖:
框架的核心類圖:
其中DOSAutoTest類用來啟動和發(fā)布JSF的mock接口,JSFMock通過動態(tài)代理的方式,實(shí)現(xiàn)下游接口的mock功能并根據(jù)測試場景獲取對應(yīng)的mock數(shù)據(jù)。
其中,mock的數(shù)據(jù)以json格式存儲在mock框架項(xiàng)目工程的指定目錄下。
該框架解決的問題:
- 省去了利用第三方mock框架如jmockit,mockito,powermock時,需要在單元測試或集成測試類中寫mock代碼的麻煩;
- 該框架模擬數(shù)據(jù)返回時,完全的模擬了接口之間的調(diào)用關(guān)系;
- 測試人員或研發(fā)人員在利用該框架mock數(shù)據(jù)時,無需額外的代碼,就可以實(shí)現(xiàn)mock數(shù)據(jù)的返回;
- 在模擬下游數(shù)據(jù)返回時,發(fā)布的mock接口調(diào)用完成后就自行銷毀,無需額外服務(wù)器進(jìn)行部署與維護(hù)。
- 在進(jìn)行接口mock時,無需在mock框架中添加相關(guān)的接口maven依賴。
單元測試展開方式
1. 單元測試應(yīng)該由誰編寫
單元測試由誰編寫?針對這個問題,大家在網(wǎng)上會找到不同的觀點(diǎn):
- 一個觀點(diǎn)是,誰寫代碼,誰自己寫單元測試。當(dāng)然,有的結(jié)對編程里面,也有相互寫的,不過,這個過程中,兩個人是共同完成的代碼。也不違反誰寫代碼誰寫單元測試的原則。
- 另一個觀點(diǎn)是單元測試應(yīng)該由其它的研發(fā)人員或測試人員來進(jìn)行編寫,理由大概可以理解為對于非代碼編寫人員來說,在設(shè)計(jì)單元測試用例的時候,對應(yīng)的是一個黑盒。在這樣的背景下,設(shè)計(jì)出來的用例覆蓋程度更高。
2. 單元測試的行業(yè)現(xiàn)狀
如果研發(fā)來負(fù)責(zé)單元測試的編寫,很多時候研發(fā)人員都不編寫單元測試。研發(fā)人員不編寫單元測試的原因其實(shí)也是比較容易理解的,因?yàn)榫帉憜卧獪y試用例工作太耗時。有時候研發(fā)的經(jīng)理或項(xiàng)目的業(yè)務(wù)方會認(rèn)為單元測試用例會減緩項(xiàng)目的整體進(jìn)度。有時候甚至整個公司層面都不認(rèn)可花費(fèi)大量的時間在單元測試上是合理的,尤其是在項(xiàng)目周期緊張和業(yè)務(wù)變動較大的項(xiàng)目上。因?yàn)閱卧獪y試從一定程度上來說確實(shí)增加的研發(fā)人員的編碼量,同時還會增加代碼的維護(hù)成本。
如果測試來負(fù)責(zé)單元測試的編寫,目前的現(xiàn)狀是測試人員需要時間理解代碼,寫單元測試的時間會變長。有代碼修改之后,在項(xiàng)目的測試壓力之下,有的測試人員,就選擇不維護(hù)單元測試,而選擇趕緊完成傳統(tǒng)的手工測試。
3. 單元測試用例自動生成
人工編寫測試用例成本增加,那么我們考慮是否可以通過自動生成的方式來實(shí)現(xiàn)單元測試呢?EvoSuite是由Sheffield等大學(xué)聯(lián)合開發(fā)的一種開源工具,用于自動生成測試用例集,生成的測試用例均符合Junit的標(biāo)準(zhǔn),可直接在Junit中運(yùn)行。
對于非業(yè)務(wù)相關(guān)的模塊,在單元測試的實(shí)踐中,就可以直接使用上述工具來自動生成單元測試代碼。雖然該工具只是輔助測試,并不能完全取代人工,測試用例的正確與否還需人工判斷,但是通過使用此自動測試工具能夠在保證代碼覆蓋率的前提下極大地提高測試人員的開發(fā)效率。
下面來詳細(xì)介紹如何使用該工具生成單元測試用例以及如何檢查單元用例的正確性。
EvoSuite為Maven項(xiàng)目提供了一個插件,該插件的具體配置如下所示:
- <plugin>
- <groupId>org.evosuite.plugins</groupId>
- <artifactId>evosuite-maven-plugin</artifactId>
- <version> ${evosuiteVersion} </version>
- <executions><execution>
- <goals>
- <goal>
- prepare
- </goal>
- </goals>
- <phase>
- process-test-classes
- </phase>
- </execution></executions>
- </plugin>
除了需要配置上述plugin外,maven還需要做如下的配置:
- <dependency>
- <groupId>org.evosuite</groupId>
- <artifactId>evosuite-standalone-runtime</artifactId>
- <version>${evosuiteVersion}</version>
- <scope>test</scope>
- </dependency>
- <!--上述依賴主要是用來自動生成單元測試用例-->
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-surefire-plugin</artifactId>
- <version>${maven-surefire-plugin-version}</version>
- <configuration>
- <systemPropertyVariables>
- <java.awt.headless>true</java.awt.headless>
- </systemPropertyVariables>
- <testFailureIgnore>true</testFailureIgnore>
- <skipTests>false</skipTests>
- <properties>
- <property>
- <name>listener</name>
- <value>org.evosuite.runtime.InitializingListener</value>
- </property>
- </properties>
- </configuration>
- </plugin>
上述plugin主要是用來混合執(zhí)行手動設(shè)計(jì)的單元測試用例和使用EvoSuite自動生成的單元測試用例。
以上EvoSuite所需的plugin和maven依賴配置完成之后,就可以使用maven命令來自動生成單元測試用例并執(zhí)行了。
- mvn -DmemoryInMB=2000 -Dcores=2 evosuite:generate evosuite:export test
生成測試用例后,可以通過人工排查生成測試用例的正確性。
寫在***
不管是研發(fā)還是測試負(fù)責(zé)集成或單元測試,選取適合自身項(xiàng)目的mock框架,一方面可以縮短測試代碼的編寫時間,另一方面可以加速測試代碼的執(zhí)行效率,同時又可以降低測試代碼的維護(hù)成本。不管是行業(yè)中通用的mock框架還是定制化的框架,都可以廣泛的應(yīng)用的測試中。
因?yàn)樽鰉ock框架不是目的,目的是為了能高效的設(shè)計(jì)出更多的測試覆蓋場景,來進(jìn)一步提升測試效率、保證產(chǎn)品質(zhì)量和將測試人員從繁重的手工測試中得以解放。
當(dāng)單元測試代碼已經(jīng)準(zhǔn)備完畢,如何才能發(fā)揮測試代碼的作用以及如何評價測試代碼的效率和做單元測試的投入產(chǎn)出比如何來衡量等等這些問題,將在后續(xù)的文章中給大家一一解答。
【本文來自51CTO專欄作者張開濤的微信公眾號(開濤的博客),公眾號id: kaitao-1234567】