上一篇中介紹了,何為測試驅動,為什麼需要測試驅動? 現在我們來看看怎樣編寫單元測試。
為了更真實的展現單元測試的魅力,我使用目前工作中的項目一段代碼(因為我太懶得在寫一段代碼。。。。),你不需要了解具體的業務,隻需要了解如何如何寫單元測試,以及感受單元測試的魅力就可以了。
這裡我們改變一種方式,即我們先寫功能代碼,再補充單元測試,很多團隊都是這樣使用,雖然這樣并不好,可是很多時候,我們新加入一個團隊,不可能負責去做一個新的項目,都是在維護老的項目,并且當時的團隊為了更快的編碼而沒有寫單元測試(這樣并不能提高效率,但是人們為了短期的利益,而這樣的事情在我們生活中處處可見。)
##功能代碼示例
簡單了解,隻需要注意 條件判斷和外部依賴(調用其他類的方法),明白我們的單元測試代碼需要覆寫到所有的條件判斷,和隔離MOCK 外部依賴。
/**
* 根據是否随機設定查詢參數
* @param params
* @param projectType
* @param prjId
* @return
*/
private Map<String, Object> setRandParam(Map<String, Object> params,Integer projectType,Integer prjId){
int allocateedAssetCountMin = (Integer)params.get("allocateedAssetCountMin");//資産池清單(前背景)最小項目已比對資産條數
int assetRandRoundCount = (Integer)params.get("assetRandRoundCount");//資産池(前背景)随機資産-每次查詢多少條
if (projectType != null && projectType.equals(ProjectType.LHB.getValue())) {
prjId = LHB_PROJECTID;
}
boolean isRand = false;//是否使用随機債權,預設不使用
//1.根據prjId确認是否需要使用随機債權
if (prjId != null) {
params.put("projectId", prjId);
int investShareCount = investRepository.queryInvtShareCountByPrjId(prjId.toString());
int allocatedAssetCount = assetRepository.queryAllocatedAssetCountByPrjId(prjId.toString());
if (investShareCount == 0 || allocatedAssetCount < allocateedAssetCountMin) {
int randAssetListCount = assetRepository.queryAssetPoolRandListCount(params);//随機債權資料總條數
isRand = true;
Integer begin = getPoolRandListBegin(randAssetListCount, prjId, PRIME_NUMBER, assetRandRoundCount);
Integer end = begin + assetRandRoundCount;
params.put("begin", begin);
params.put("end", end);
}
}
MapUtil.addValueToMap(params, "isRand", isRand);
return params;
}
這段代碼中,有條件判斷,和依賴外部(資料庫)。我們需要對這個做單元測試,要保證不受外界影響(資料庫的結果)是以我們需要 MOCK(模拟外部調用傳回的資料)
首先為了能夠 運作單元測試我們需要引入 junit(我使用過的是Java,其他語言有其他的測試架構)
因為我們需要 @test 注解,和強大的斷言 Assert .
<!-- https://mvnrepository.com/artifact/junit/junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
為了mock 外部的依賴,我們需要使用mock 工具,這裡使用 powerMock, 引入相關的 依賴包(maven搜尋,這裡不再展示)。
為了能夠MOCK ,我們在測試類上面使用注解 @RunWith(PowerMockRunner.class) 代表這個類需要使用mock。
在類中對 測試的類使用 @injectMocks 注解我們測試的類, @mock 注解我們外部依賴的類。
@InjectMocks
private AssetServiceFacade assetServiceFacade;
@Mock
private InvestRepository investRepository;
測試類的命名要求 類名+Test ,測試的方法 test+方法名,編寫的測試用例上面 注解@Test ,junit就知道了這個方法是測試用例(畢竟測試類中還存在一些資料準備的方法)。
現在來看看,測試用代碼:
根據功能代碼中的條件判斷,我們需要寫三個用例 (這裡被測試代碼由于是私有的,編寫比較特殊需要使用反射來擷取方法,公共方法,直接調用類就可以了。)
##用例一:條件為NULL
@Test
public void testSetRandParamWithNull() throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException{
//準備資料
Map<String, Object> params = Maps.newHashMap();
Integer prjId = null;
Integer projectType = null;
MapUtil.addValueToMap(params, "allocateedAssetCountMin", 500);
MapUtil.addValueToMap(params, "assetRandRoundCount", 3000);
//使用反射,調用方法
Class assetFacade = assetServiceFacade.getClass();
Method setRandParam = assetFacade.getDeclaredMethod("setRandParam",Map.class,Integer.class,Integer.class);
setRandParam.setAccessible(true);
params = (Map<String, Object>) setRandParam.invoke(assetServiceFacade, params,projectType,prjId);
Assert.assertEquals(false, params.get("isRand"));
}
用例二
@Test
public void testSetRandParamWithLHB() throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException{
//準備資料
Map<String, Object> params = Maps.newHashMap();
Integer prjId = 3;
Integer projectType = 104040; //靈活寶
MapUtil.addValueToMap(params, "allocateedAssetCountMin", 500);
MapUtil.addValueToMap(params, "assetRandRoundCount", 3000);
//使用反射,調用方法
Class assetFacade = assetServiceFacade.getClass();
Method setRandParam = assetFacade.getDeclaredMethod("setRandParam", Map.class,Integer.class,Integer.class);
setRandParam.setAccessible(true);
//mock
when(investRepository.queryInvtShareCountByPrjId(anyString())).thenReturn((Integer)20);
when(assetRepository.queryAllocatedAssetCountByPrjId(anyString())).thenReturn(600);
params = (Map<String, Object>) setRandParam.invoke(assetServiceFacade, params,projectType,prjId);
Assert.assertEquals(1, params.get("projectId"));
Assert.assertEquals(false, params.get("isRand"));
}
用例三
@Test
public void testSetRandParamWithRand() throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException{
//準備資料
Map<String, Object> params = Maps.newHashMap();
Integer prjId = 3;
Integer projectType = 104040;
MapUtil.addValueToMap(params, "allocateedAssetCountMin", 500);
MapUtil.addValueToMap(params,"assetRandRoundCount", 3000);
//使用反射,調用方法
Class assetFacade = assetServiceFacade.getClass();
Method setRandParam = assetFacade.getDeclaredMethod("setRandParam", Map.class,Integer.class,Integer.class);
setRandParam.setAccessible(true);
when(investRepository.queryInvtShareCountByPrjId(anyString())).thenReturn(20);
when(assetRepository.queryAllocatedAssetCountByPrjId(anyString())).thenReturn(20);
params = (Map<String, Object>) setRandParam.invoke(assetServiceFacade, params,projectType,prjId);
Assert.assertEquals(true, params.get("isRand"));
}
測試步驟
可以看到,上面三個的用例的大緻步驟都是:
1.準備資料 (方法的參數)
2.mock相關的外部依賴,直接給出我們結果,when(調用方法).thenReturn(結果)。 當調用這個方法,直接傳回 期望的結果。
3.斷言判斷關鍵結果是否符合我們的期望。(如果不符合,則修改功能代碼,前提保證測試的邏輯是沒有問題的。)
運作測試
這個時候在單元測試右鍵 Run as -> Junit Test ,可以看到結果,我的這個,我已經運作成功,但是不認為這是一次就運作成功。事情都是曲折發展,在你編寫單元測試的過程中(我們這種方式先編碼,再補單元測試)會經常 修改單元測試代碼保證單元測試代碼沒有問題,如果之後測試還跑不通,那麼這個時候大膽的修改代碼,因為你有單元測試這個強大的工具保駕護航。
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIyZuBHL0FWby9mZvwVZnFWbp1zczV2YvJHctM3cv1Ce-c3ZU1UeNR1T4VkaNVTQE90djRVT3lkeMBjVtJWd0ckW65UbM5WOHJWa1knW0xmMMZ3bENGMShUYvwlbj5yZtlmbkN3YuQnclZnbvN2Ztl2Lc9CX6MHc0RHaiojIsJye.jpg)
測試覆寫率
測試完成之後,你還需要保證,你的單元測試代碼的沒有忘記哪個分支條件,此時使用 檢測覆寫率工具 EclEmma
進行覆寫的檢查。
安裝完 EclEmma ,插件右鍵 coverage as —> junit test ,可以看到我們的功能代碼,有了顔色标記,綠色代表 測試代碼覆寫到了,紅色代表沒有覆寫到,黃色代表沒有完全覆寫。
##總結
那麼現在我們總結一下,如果編寫單元測試代碼:
- 準備 junit 測試架構
- 建立測試類
- 引入 mock工具,mock外部依賴,我們這裡使用 powerMock,你可以選擇其他的。
- 檢測單元測試覆寫率