Mock框架的三次迭代
編輯推薦: |
本文來自於51cto,文章主要講解Mock框架三個版本的實現方法和優缺點以及單元測試展開方式等相關內容。 |
如何定義單元
對於單元測試中的單元,不同的人有不同的看法:可以理解為一個方法,可以理解為一個完整的介面實現,也可以理解為一個完整的功能模組或者是多個功能模組的一個耦合。
根據以往的單元測試經驗,在設計單元測試用例時,當針對方法級別展開單元測試時,重點關注的是方法的底層邏輯;當針對的是模組時,針對的是實際的業務邏輯實現;當針對整合後的模組進行測試時,一般稱之為整合測試。
不管是單元測試還是整合測試,都可以統一的理解為單元測試。因為他們的本質都是對方法或介面的一種測試形式,只是所處的階段不一樣罷了。
1. 整合測試應該由誰編寫
在我們的實際工作中,研發人員在提交程式碼之前,會設計一些“冒煙測試”級別整合測試用例。等到整個功能開發完成後,測試人員會根據業務需求和設計的測試用例,來進行整體的整合測試用例的編寫、執行、失敗用例分析,以及程式碼的調式和問題程式碼的定位等工作。
2. 整合測試用例
業務相關的測試主要是通過spring-test來進行整合測試,基本的測試結構為先定義一個基類用來初始化被測試類。
測試基類定義結構如下:
@RunWith(SpringJUnit4ClassRunner.class)
ContextConfiguration(locations = {"classpath:./spring/applicationContext.xml"})
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
public class BaseSpringJunitTest {
@Autowired
protected BusinessRelatedServiceImpl businessRelatedService;
}
業務相關的測試類定義如下格式:
public class BusinessRelatedServiceImplDomainTest extends BaseSpringJunitTest {
@Test
public void testScenario1 (){
new Thread(new DOSAutoTest("testScenario1")).start();
Thread.sleep(1000*60*1);
String requestJson=""//測試入參;
RequestPojo request=( RequestPojo )JSONUtils.jsonToBean(requestJson,RequestPojo .class);
ResponsePojo response= businessRelatedService.businessRelatedMethod(ResponsePojo );
//業務相關的assert區域
}
}
3. 如何解決下游系統依賴
businessRelatedMethod方法在處理業務邏輯的過程中需要呼叫下游JSF(Jingdong Service Framework,完全自主研發的高效能RPC服務框架)提供的訂單介面(OrderverExportService),並根據入參中的訂單編號獲取訂單的詳細資訊(ResultPojo getOrderInfoById (long orderId))。
那麼如何獲取下游JSF介面的返回正確資料就變成了一個比較重要的問題。如果是在功能測試或者聯調測試階段,可以由下游測試人員來提供資料。不過這樣溝通和測試成本較高,無法滿足業務快速上線和變化的要求,尤其在整合測試階段這個問題就變得尤為明顯,因為下游資料對於上游來說是不可控的。這樣mock下游資料就變得尤為緊急和重要。
4. Mock框架的選擇
在整個java生態圈中,支援mock的開源框架還是比較多的,比如常用的mockito、powermock、easymock和jmockit等開源框架。這些框架在mock方面都具有比較強大的功能與比較廣泛的使用量。但是這些框架都具有一個相同的缺點,那就是需要或多或少的編碼工作來mock所需要的介面返回資料。
在設計mock框架的時候,我們考慮到儘量讓寫單元測試的人員或研發人員少編碼或不編碼,來獲取不同的業務場景所需要的測試資料。
Mock框架 第一版
該版本的mock框架的整體思想為:結合JSF的特性,Override所有下游介面的方法,然後將實現下游介面的應用部署到測試環境,釋出一個有別與真實下游介面的服務,在介面呼叫的時候,通過不同的JSF介面別名來進行區分。Mock的資料儲存在資料庫中。
該框架類呼叫關係:
Mock介面的具體實現:
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需要的資料
result.setReturnObject(orderver);
return result;
}
}
Mock服務釋出完後的效果:
在整合測試階段,只需要修改該介面的JSF別名,就可以實現該介面的mock呼叫。
<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
該框架的優缺點
優點:
做整合測試用例設計時,不用編寫程式碼,只需要維護測試場景所需要的返回資料;
該框架不僅可以用在整合測試中,在下游介面無變更的前提下,同時還可以用在後續系統測試與聯調測試階段。
缺點:
mock服務的釋出依賴於伺服器與資料庫,當依賴的伺服器或資料庫出現跌機情況時,該mock服務不用;
該框架的維護成本比較大,當下遊依賴的介面較多時,所有的服務包含的方法均需要進行override;
當下遊的介面定義發生變化時比如新增介面方法,該mock服務需要重新override該新增的方法並且需要重新打包部署;
下游介面方法的資料結構發生變化時,儲存資料的資料表結構需要做相應的調整,對於業務變化較快的系統,這種型別的改動頻率還是較高。
Mock框架 第二版
為了解決上述mock框架依賴伺服器與資料庫的問題,我們又做了第二次嘗試。將mock框架設計為jar包的形式,提供給程式來呼叫。在下游介面的實現方式上第二版與第一版保持不變,同時業務資料不放資料庫,而是將業務資料放到檔案中。變化的點為介面呼叫上需要將對應的jsf:comsumer節點替換為對應的實際mock的實現類。
Mock介面的實現:
@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需要的資料
result.setReturnObject(orderver);
return result;
}
}
Mock介面呼叫配置:
<!--<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>
該框架的優缺點
優點:
做整合測試用例設計時,不用編寫程式碼,只需要維護測試場景所需要的返回資料;
相比較第一個版本,該版本在執行效率上有了較大的提升,因為mock類的載入是走的本地Spring配置檔案,同時資料載入也是走的本地檔案;
無需再依賴於伺服器部署和資料庫依賴。
缺點:
該框架的維護成本比較大,當下遊依賴的介面較多時,所有的服務包含的方法均需要進行override;
當下遊的介面定義發生變化時比如新增介面方法,該mock服務需要重新override該新增的方法並且需要重新打包,然後上傳到maven倉庫;
下游介面方法的資料結構發生變化時,對於業務變化較快的系統,這種型別的改動頻率還是較高。
Mock框架 第三版
隨著需要mock的介面變的越來越龐大,以上兩種mock框架的實現的缺點就變的越來越突出。該框架可以說從根本上解決了上述框架實現的問題。因為該框架充分利用了JDK的動態代理,反射機制以及JSF提供的高階特性來實現我們的mock框架。框架維護任務可以做到無需做更多的針對介面的編碼任務。測試人員只需要將重點放在測試資料的準備上。
框架整體呼叫時序圖:
框架的核心類圖:
其中DOSAutoTest類用來啟動和釋出JSF的mock介面,JSFMock通過動態代理的方式,實現下游介面的mock功能並根據測試場景獲取對應的mock資料。
其中,mock的資料以json格式儲存在mock框架專案工程的指定目錄下。
該框架解決的問題:
省去了利用第三方mock框架如jmockit,mockito,powermock時,需要在單元測試或整合測試類中寫mock程式碼的麻煩;
該框架模擬資料返回時,完全的模擬了介面之間的呼叫關係;
測試人員或研發人員在利用該框架mock資料時,無需額外的程式碼,就可以實現mock資料的返回;
在模擬下游資料返回時,釋出的mock介面呼叫完成後就自行銷燬,無需額外伺服器進行部署與維護。
在進行介面mock時,無需在mock框架中新增相關的介面maven依賴。
單元測試展開方式
1. 單元測試應該由誰編寫
單元測試由誰編寫?針對這個問題,大家在網上會找到不同的觀點:
一個觀點是,誰寫程式碼,誰自己寫單元測試。當然,有的結對程式設計裡面,也有相互寫的,不過,這個過程中,兩個人是共同完成的程式碼。也不違反誰寫程式碼誰寫單元測試的原則。
另一個觀點是單元測試應該由其它的研發人員或測試人員來進行編寫,理由大概可以理解為對於非程式碼編寫人員來說,在設計單元測試用例的時候,對應的是一個黑盒。在這樣的背景下,設計出來的用例覆蓋程度更高。
2. 單元測試的行業現狀
如果研發來負責單元測試的編寫,很多時候研發人員都不編寫單元測試。研發人員不編寫單元測試的原因其實也是比較容易理解的,因為編寫單元測試用例工作太耗時。有時候研發的經理或專案的業務方會認為單元測試用例會減緩專案的整體進度。有時候甚至整個公司層面都不認可花費大量的時間在單元測試上是合理的,尤其是在專案週期緊張和業務變動較大的專案上。因為單元測試從一定程度上來說確實增加的研發人員的編碼量,同時還會增加程式碼的維護成本。
如果測試來負責單元測試的編寫,目前的現狀是測試人員需要時間理解程式碼,寫單元測試的時間會變長。有程式碼修改之後,在專案的測試壓力之下,有的測試人員,就選擇不維護單元測試,而選擇趕緊完成傳統的手工測試。
3. 單元測試用例自動生成
人工編寫測試用例成本增加,那麼我們考慮是否可以通過自動生成的方式來實現單元測試呢?EvoSuite是由Sheffield等大學聯合開發的一種開源工具,用於自動生成測試用例集,生成的測試用例均符合Junit的標準,可直接在Junit中執行。
對於非業務相關的模組,在單元測試的實踐中,就可以直接使用上述工具來自動生成單元測試程式碼。雖然該工具只是輔助測試,並不能完全取代人工,測試用例的正確與否還需人工判斷,但是通過使用此自動測試工具能夠在保證程式碼覆蓋率的前提下極大地提高測試人員的開發效率。
下面來詳細介紹如何使用該工具生成單元測試用例以及如何檢查單元用例的正確性。
EvoSuite為Maven專案提供了一個外掛,該外掛的具體配置如下所示:
<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主要是用來混合執行手動設計的單元測試用例和使用EvoSuite自動生成的單元測試用例。
以上EvoSuite所需的plugin和maven依賴配置完成之後,就可以使用maven命令來自動生成單元測試用例並執行了。
生成測試用例後,可以通過人工排查生成測試用例的正確性。
寫在最後
不管是研發還是測試負責整合或單元測試,選取適合自身專案的mock框架,一方面可以縮短測試程式碼的編寫時間,另一方面可以加速測試程式碼的執行效率,同時又可以降低測試程式碼的維護成本。不管是行業中通用的mock框架還是定製化的框架,都可以廣泛的應用的測試中。
因為做mock框架不是目的,目的是為了能高效的設計出更多的測試覆蓋場景,來進一步提升測試效率、保證產品質量和將測試人員從繁重的手工測試中得以解放。