成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

安卓單元測(cè)試全攻略,讓代碼測(cè)試一勞永逸

移動(dòng)開(kāi)發(fā) Android
安卓單元測(cè)試,只看這一篇就足夠啦。真正的完全解析,真正的從0到1,Junit結(jié)合Mockito與Robolectric實(shí)現(xiàn)從M到V再到P,Jacoco掃描函數(shù)、邏輯、代碼行數(shù)單元測(cè)試覆蓋率100%的全面測(cè)試。

前言

安卓單元測(cè)試,只看這一篇就足夠啦。真正的完全解析,真正的從0到1,Junit結(jié)合Mockito與Robolectric實(shí)現(xiàn)從M到V再到P,Jacoco掃描函數(shù)、邏輯、代碼行數(shù)單元測(cè)試覆蓋率100%的全面測(cè)試。你是否還在為了驗(yàn)證聯(lián)網(wǎng)與未聯(lián)網(wǎng)狀態(tài)而頻繁的開(kāi)關(guān)WiFi開(kāi)關(guān)?或者你是否還在為一個(gè)switch判斷而頻繁的使用debug斷點(diǎn)setValue來(lái)觀測(cè)代碼的邏輯判斷情況?又或者你是否還在為了校驗(yàn)?zāi)硞€(gè)UI文案的正確性而反復(fù)的比對(duì)UI稿?可能你會(huì)反問(wèn),難道寫(xiě)完代碼自測(cè)也有錯(cuò)?當(dāng)然不是,自測(cè)是一個(gè)良好的習(xí)慣,不過(guò)作為一名工程師,你要做的不應(yīng)該只是看看點(diǎn)點(diǎn)的黑盒測(cè)試,而是應(yīng)該設(shè)計(jì)出一套能夠讓代碼測(cè)試代碼,一勞永逸的測(cè)試工程。

正文

首先我們從Model層開(kāi)始,通過(guò)具體代碼來(lái)詳盡說(shuō)明一下一個(gè)單元測(cè)試覆蓋率100%的測(cè)試工程是如何建立的。嚴(yán)格意義上講,Model數(shù)據(jù)層負(fù)責(zé)數(shù)據(jù)加載與儲(chǔ)存,是游離于安卓環(huán)境之外的存在,所以它可以不需要借助安卓SDK的支持。使用Junit結(jié)合Mockito即可做到100%條件分支覆蓋率的單元測(cè)試。如果項(xiàng)目的Model層有安卓依賴,可能就表明此處的代碼需要重構(gòu)了,這也是單元測(cè)試其中的一個(gè)意義,讓代碼邏輯更清晰。清除Model層的安卓依賴的另一層面好處是讓測(cè)試case更高效,含有android依賴的測(cè)試case執(zhí)行最快也需要5秒,但對(duì)于一個(gè)沒(méi)有安卓依賴的Model類(lèi),跑完全部case的時(shí)間可以降低至毫秒級(jí)。所以,去除Model層所不需要的安卓依賴還是很有必要的。

 

代碼

Model層測(cè)試代碼如下:

  1. @RunWith(MockitoJUnitRunner.class) 
  2.  
  3. public classWeatherModelTest { 
  4.  
  5.     privateWeatherModelmodel; 
  6.  
  7.     @Mock 
  8.  
  9.     ApiServiceapi; 
  10.  
  11.     @Mock 
  12.  
  13.     WeatherDataConvertconvertData; 
  14.  
  15.     @Mock 
  16.  
  17.     WeatherRequestListenerlistener; 
  18.  
  19.     private static finalStringJSON_ROOT_PATH="/json/"
  20.  
  21.     privateStringjsonFullPath; 
  22.  
  23.     privateWeatherDatanetData; 
  24.  
  25.     privateMapqueryMap; 
  26.  
  27.     @Before 
  28.  
  29.     public voidsetUp() { 
  30.  
  31.         RxUnitTestTools.openRxTools(); 
  32.  
  33.         model=newWeatherModel(); 
  34.  
  35.     } 
  36.  
  37.     private voidinitResponse() { 
  38.  
  39.         try{ 
  40.  
  41.             jsonFullPath= getClass().getResource(JSON_ROOT_PATH).toURI().getPath(); 
  42.  
  43.         } 
  44.  
  45.         catch(URISyntaxException e) { 
  46.  
  47.             e.printStackTrace(); 
  48.  
  49.         } 
  50.  
  51.         String json = getResponseString("weather.json"); 
  52.  
  53.         Gson gson =newGson(); 
  54.  
  55.         netData= gson.fromJson(json,WeatherData.class); 
  56.  
  57.         model.setApiService(api); 
  58.  
  59.         try{ 
  60.  
  61.             Field field = WeatherModel.class.getDeclaredField("convert"); 
  62.  
  63.             field.setAccessible(true); 
  64.  
  65.             field.set(model,convertData); 
  66.  
  67.         } 
  68.  
  69.         catch(Exception e) { 
  70.  
  71.             //reflect error 
  72.  
  73.         } 
  74.  
  75.         queryMap=newHashMap<>(); 
  76.  
  77.         queryMap.put("city","沈陽(yáng)"); 
  78.  
  79.     } 
  80.  
  81.     privateStringgetResponseString(String fileName) { 
  82.  
  83.         returnFileUtil.readFile(jsonFullPath+ fileName,"UTF-8").toString(); 
  84.  
  85.     } 
  86.  
  87.     private voidsetFinalStatic(Field field,Object newValue)throwsException { 
  88.  
  89.         field.setAccessible(true); 
  90.  
  91.         Field modifiersField = Field.class.getDeclaredField("modifiers"); 
  92.  
  93.         modifiersField.setAccessible(true); 
  94.  
  95.         modifiersField.setint(field,field.getModifiers() & ~Modifier.FINAL); 
  96.  
  97.     } 
  98.  

首先通過(guò)@Mock注解對(duì)需要mock的對(duì)象進(jìn)行初始化,然后我們需要對(duì)測(cè)試類(lèi)進(jìn)行測(cè)試case分析,WeatherModelmode類(lèi)是一個(gè)網(wǎng)絡(luò)請(qǐng)求數(shù)據(jù)model,所以這個(gè)model類(lèi)的核心是request函數(shù)。首先對(duì)request函數(shù)進(jìn)行分析。必須涵蓋的測(cè)試點(diǎn)如下:請(qǐng)求參數(shù)校驗(yàn),請(qǐng)求成功且返回碼正確處理邏輯校驗(yàn),請(qǐng)求成功但校驗(yàn)碼錯(cuò)誤處理邏輯校驗(yàn)和請(qǐng)求失敗處理邏輯校驗(yàn)。同時(shí)Model類(lèi)中還有一個(gè)觀察者解綁函數(shù),所以測(cè)試case也需要包含解綁函數(shù)處理邏輯測(cè)試這一項(xiàng)。通過(guò)initResponse,我們可以對(duì)接口返回值進(jìn)行模擬,這里采用讀Json文件的方法將接口返回做成Json數(shù)據(jù)文件,結(jié)合服務(wù)端的Swagger文檔可以很輕易的實(shí)現(xiàn)服務(wù)端接口數(shù)據(jù)模擬。

  1. @Test@SuppressWarnings("unchecked")public voidtestParams() { 
  2.  
  3.     model.request(listener,"沈陽(yáng)"); 
  4.  
  5.     try{ 
  6.  
  7.         Field fieldParam = WeatherModel.class.getDeclaredField("queryMap"); 
  8.  
  9.         Field fieldKey = WeatherModel.class.getDeclaredField("CITY"); 
  10.  
  11.         fieldParam.setAccessible(true); 
  12.  
  13.         setFinalStatic(fieldKey, true); 
  14.  
  15.         Map queryMaps = (Map) fieldParam.get(model); 
  16.  
  17.         String key = (String) fieldKey.get(model); 
  18.  
  19.         assertEquals("驗(yàn)證queryMap的Key",key,"city"); 
  20.  
  21.         String city = queryMaps.get("city"); 
  22.  
  23.         assertEquals("驗(yàn)證queryMap的value",city,"沈陽(yáng)"); 
  24.  
  25.     } 
  26.  
  27.     catch(Exception e) { 
  28.  
  29.         //reflect error} 
  30.  
  31.     } 

對(duì)于有參數(shù)的Api,***步就是驗(yàn)證傳參。可能你會(huì)覺(jué)得大材小用,但眾多的血淋淋的慘案告訴我們?cè)绞羌?xì)小的東西越容易產(chǎn)生問(wèn)題,而單元測(cè)試就是幫助我們將細(xì)小的問(wèn)題解決在編碼時(shí)期而不對(duì)外暴露。要驗(yàn)證參數(shù)的正確性,首先我們需要要驗(yàn)證向queryMap中put的時(shí)候是否正確。對(duì)于queryMap,我們需要驗(yàn)證K-V鍵值對(duì)的正確性,還是那句防微杜漸,因?yàn)閝ueryMap是一個(gè)private變量,在正常情況下我們無(wú)法獲取到它的值,而為這個(gè)變量加一個(gè)對(duì)業(yè)務(wù)毫無(wú)用處的get/set方法就顯得太刻意了,我們的目的是為了解決讓代碼更健壯,bug更少,而不是為了測(cè)試而測(cè)試。拿不到queryMap參數(shù)測(cè)試還怎么進(jìn)行?難道單元測(cè)試也要從入門(mén)到放棄?要其實(shí)很多事情都是這樣,當(dāng)你覺(jué)得某個(gè)問(wèn)題完全沒(méi)有辦法解決的時(shí)候,一定是你考慮的不夠周全。queryMap對(duì)象的值我們可以通過(guò)Java反射獲得。反射的原理在這里我就不為大家闡述了,在testParams方法中,我們首先通過(guò)getDeclaredField獲取了queryMap對(duì)象,然后我們需要獲得到put的key。key的獲得使我們陷入了第二個(gè)難題,可能你會(huì)說(shuō),這有什么難的,繼續(xù)反射啊,可這個(gè)key是一個(gè)private static變量,通過(guò)正常的反射是無(wú)法拿到key的,最多會(huì)拿到一個(gè)異常。還是那句,不要放棄尋找解決方案,最終我們發(fā)現(xiàn)只要設(shè)置下虛擬機(jī)不去檢測(cè)私有屬性,即可完成對(duì)private static變量的獲取。不要覺(jué)得只是很小的一個(gè)參數(shù),這么勞師動(dòng)眾不值得,據(jù)不完全統(tǒng)計(jì),每天因?yàn)榻涌趉ey值多寫(xiě)或是寫(xiě)錯(cuò)一個(gè)字母而產(chǎn)生的bug不計(jì)其數(shù)。

  1. @Test@SuppressWarnings("unchecked")public voidtestRequestSuccess() { 
  2.  
  3.     initResponse(); 
  4.  
  5.     Mockito.when(api.getWeather(queryMap)).thenReturn(Observable.just(netData)); 
  6.  
  7.     ArgumentCaptor captor = ArgumentCaptor.forClass(WeatherData.class); 
  8.  
  9.     model.request(listener,"沈陽(yáng)"); 
  10.  
  11.     Mockito.verify(api).getWeather(queryMap); 
  12.  
  13.     Mockito.verify(listener).showLoading(); 
  14.  
  15.     Mockito.verify(listener).hideLoading(); 
  16.  
  17.     Mockito.verify(convertData).convertData(captor.capture()); 
  18.  
  19.     WeatherData result = captor.getValue(); 
  20.  
  21.     intstatus = result.getStatus(); 
  22.  
  23.     assertEquals("驗(yàn)證code",status,1000); 
  24.  

保證的參數(shù)傳遞的前提下,我們接下來(lái)需要對(duì)接口返回狀態(tài)進(jìn)行測(cè)試,首先便是成功態(tài)的接口返回。Mockito.when的作用是設(shè)定預(yù)期返回結(jié)果,例如case testRequestSuccess()所要測(cè)試的是請(qǐng)求成功且返回碼正確的情況,所以我們對(duì)response的預(yù)期就是讓它執(zhí)行onNext方法,同時(shí)返回我們初始化好的完全正確的接口數(shù)據(jù)。Mockito.when使得測(cè)試代碼可以完全按照我們所預(yù)期的執(zhí)行。不過(guò)這個(gè)聲明必須在方法執(zhí)行之前,即Mockito.when必須比model.request(listener,"沈陽(yáng)");先執(zhí)行才會(huì)生效。Junit提供了豐富的assert斷言機(jī)制,借助assert我們可以實(shí)現(xiàn)多種情況的測(cè)試,然而對(duì)于沒(méi)有明確返回值的void方法,assert就顯得有些無(wú)能為力,因?yàn)樗鼰o(wú)法找到一個(gè)標(biāo)準(zhǔn)進(jìn)行斷言。這時(shí)候需要使用mockito的verify方法,它的作用是驗(yàn)證mock對(duì)象的某一個(gè)方法是否得到了正確的執(zhí)行Mockito.verify(listener).showLoading();就是驗(yàn)證加載進(jìn)度條是否能夠正常顯示,ArgumentCaptor是一個(gè)參數(shù)捕獲,它可以捕獲onNext返回的數(shù)據(jù),通過(guò)assert斷言,我們可以驗(yàn)證成功情況下數(shù)據(jù)是否正確。數(shù)據(jù)成功情況下,我們有一個(gè)網(wǎng)絡(luò)數(shù)據(jù)向視圖數(shù)據(jù)轉(zhuǎn)換的過(guò)程,這個(gè)轉(zhuǎn)換方法是在convert類(lèi)中執(zhí)行的操作,因?yàn)槲覀冏龅氖菃卧獪y(cè)試而非集成測(cè)試,所以基于WeatherModel這個(gè)測(cè)試類(lèi),我們只需要驗(yàn)證到convertData()這個(gè)函數(shù)是否正確得到了調(diào)用即可,數(shù)據(jù)轉(zhuǎn)換的內(nèi)容由Convert類(lèi)的單元測(cè)試進(jìn)行跟蹤即可。

  1. @Testpublic voidtestStatusError() { 
  2.  
  3.     initResponse(); 
  4.  
  5.     netData.setStatus(1001); 
  6.  
  7.     Mockito.when(api.getWeather(queryMap)).thenReturn(Observable.just(netData)); 
  8.  
  9.     ArgumentCaptor captor = ArgumentCaptor.forClass(WeatherData.class); 
  10.  
  11.     model.request(listener,"沈陽(yáng)"); 
  12.  
  13.     Mockito.verify(api).getWeather(queryMap); 
  14.  
  15.     Mockito.verify(listener).showLoading(); 
  16.  
  17.     Mockito.verify(listener).fail(null,ServerCode.get(netData.getStatus()).getMessage()); 
  18.  

在實(shí)際開(kāi)發(fā)過(guò)程中,服務(wù)端通常會(huì)對(duì)同一接口的不同狀態(tài)做成不同的服務(wù)應(yīng)答碼,雖然返回非常態(tài)應(yīng)答碼的時(shí)候網(wǎng)絡(luò)請(qǐng)求也是成功,但它卻是有別于常態(tài)服務(wù)端應(yīng)答的另一種情況。所以,這里需要對(duì)非常態(tài)服務(wù)應(yīng)答碼進(jìn)行一個(gè)條件分支的測(cè)試。testStatusError ()的測(cè)試方法與testRequestSuccess()類(lèi)似,只是我們這次的status模擬值由成功的status換成了一個(gè)異常status,同時(shí),驗(yàn)證的函數(shù)執(zhí)行也變成了listener的失敗方法

  1. @Testpublic voidtestRequestFail() { 
  2.  
  3.     initResponse(); 
  4.  
  5.     Exception exception =newException("exception"); 
  6.  
  7.     Mockito.when(api.getWeather(queryMap)).thenReturn(Observable.error(exception)); 
  8.  
  9.     model.request(listener,"沈陽(yáng)"); 
  10.  
  11.     Mockito.verify(listener).fail(null,"exception"); 
  12.  

Request是一個(gè)接口,我們不能夠保證每次請(qǐng)求我們的服務(wù)器都能夠給與準(zhǔn)確應(yīng)答,同時(shí)用戶在發(fā)出請(qǐng)求的時(shí)候我們也不能夠保證用戶所處的網(wǎng)絡(luò)狀態(tài)是否通暢。所以我們?cè)谠O(shè)計(jì)Model類(lèi)的時(shí)候也要將非常態(tài)考慮在內(nèi),對(duì)接口的異常情況進(jìn)行處理,有時(shí)候我們需要自己創(chuàng)造一些異常來(lái)驗(yàn)證我們代碼的健壯程度。同樣的,我們的測(cè)試類(lèi)也需要有一個(gè)專(zhuān)門(mén)的方法來(lái)保證異常態(tài)的測(cè)試。testRequestFail()的測(cè)試方法與成功的方法的不同之處在于我們首先我們需要mock的不是接口數(shù)據(jù),而是一個(gè)異常,Exception exception = new Exception("exception");注意,這個(gè)Exception中的參數(shù)即是異常信息,因?yàn)槲覀兊膄ail方法中有異常信息的顯示,所以這個(gè)參數(shù)是必須要加上的,否則e.getLocalizedMessage()會(huì)拋出NPE。另外,這個(gè)時(shí)候的Mockito.when的期望也有所改變,這次我們期望的是函數(shù)執(zhí)行onError方法。

  1. @Testpublic voidtestCancelRequest() { 
  2.  
  3.     Subscription subscription =mock(Subscription.class); 
  4.  
  5.     model.setSubscription(subscription); 
  6.  
  7.     model.cancelRequest(); 
  8.  
  9.     verify(subscription).unsubscribe(); 
  10.  

Model類(lèi)中***一個(gè)case是testCancelRequest()它的作用是,在合適的時(shí)候解綁request,我們的網(wǎng)絡(luò)請(qǐng)求是異步的,也就是說(shuō)當(dāng)我們調(diào)用請(qǐng)求的activity或是fragment destroy的時(shí)候,如果我們沒(méi)有解除綁定,是存在內(nèi)存泄漏風(fēng)險(xiǎn)的。當(dāng)然,我們能想到的問(wèn)題,Rxjava的維護(hù)者們也一定想到了,Subscription就是方便我們?cè)谏芷诮Y(jié)束的時(shí)候?qū)x解綁。驗(yàn)證方法很簡(jiǎn)單,還是通過(guò)verify方法,驗(yàn)證解綁方法是否得到了正確執(zhí)行。

  1. dependencies { 
  2.  
  3.     classpath'com.vanniktech:gradle-android-junit-jacoco-plugin:0.6.0' 
  4.  

至此我們已經(jīng)完成了對(duì)model的全覆蓋測(cè)試,點(diǎn)擊測(cè)試類(lèi)前面的運(yùn)行按鈕,可以看到所有測(cè)試類(lèi)運(yùn)行的情況,綠色代表成功,紅色代表存在問(wèn)題,可以通過(guò)下方的Log日志查看引起測(cè)試失敗的問(wèn)題點(diǎn)進(jìn)行改正,借助Jacoco統(tǒng)計(jì)工具可以看到單元測(cè)試覆蓋率的情況。之所以選擇使用Jacoco而不是IDE自帶的Coverage是因?yàn)樵跍y(cè)試&條件分支的情況下Coverage存在漏洞,導(dǎo)致沒(méi)有達(dá)到全覆蓋的測(cè)試顯示已覆蓋完全。Jacoco的AndroidStudio集成網(wǎng)絡(luò)資源并不多,集成方法不是存在潛在漏洞就是過(guò)于繁瑣。經(jīng)過(guò)兩天的不斷搜索,終于發(fā)現(xiàn)了一個(gè)史上最簡(jiǎn)單集成方法,只需要在主工程的gradle文件中添加一個(gè)Jacoco插件,gradle就會(huì)生成一個(gè)Jacoco Task,雙擊運(yùn)行即可生成一份Html覆蓋率報(bào)告。運(yùn)行我們的model測(cè)試類(lèi),從jacoco生成的html可以看到,我們的model已經(jīng)達(dá)到了100%的全覆蓋。既然如此,我們是不是就可以認(rèn)為MVP的M層已經(jīng)ok了呢?等等,我們好像遺漏了點(diǎn)什么,沒(méi)錯(cuò),onNext情況下的數(shù)據(jù)轉(zhuǎn)換類(lèi)還沒(méi)有測(cè)試,下面我們來(lái)對(duì)convert類(lèi)進(jìn)行一下測(cè)試。

首先們來(lái)看看convert類(lèi)代碼:

  1. /** 
  2.  
  3. * Author : YangHaoyi on 2017/6/28. 
  4.  
  5. * Email  :  yanghaoyi@neusoft.com 
  6.  
  7. * Description :網(wǎng)絡(luò)數(shù)據(jù)與視圖數(shù)據(jù)轉(zhuǎn)換器 
  8.  
  9. * Change : YangHaoYi on 2017/6/28. 
  10.  
  11. * Version : V 1.0 
  12.  
  13. */ 
  14.  
  15. open classWeatherDataConvert { 
  16.  
  17.     open funconvertData(netData: WeatherData):WeatherViewData{ 
  18.  
  19.         valviewData= WeatherViewData() 
  20.  
  21.         viewData.temperature= netData.data?.temperature?:0.0viewData.weatherType= netData.data?.weatherType?:1viewData.ultraviolet= netData.data?.ultraviolet?:0viewData.rainfall= netData.data?.rainfall?:"0"viewData.hourTemperature= netData.data?.hourTemperature?:"10"viewData.windPower= netData.data?.windPower?:"2"returnviewData 
  22.  
  23.         } 
  24.  

從代碼可以看出我們的convert類(lèi)看起來(lái)有一些的奇怪,每錯(cuò),因?yàn)樗⒉皇莏ava代碼,它是kotlin。好好的java工程為什么要混入kotlin,單單只是為了炫技么?當(dāng)然不是,數(shù)據(jù)轉(zhuǎn)換類(lèi)的作用是對(duì)網(wǎng)絡(luò)數(shù)據(jù)進(jìn)行判空并包裝成視圖數(shù)據(jù),我們都知道在java中的判空,需要層層嵌套,例如,我們需要判斷Student類(lèi)中的Score類(lèi)中的EnglishScore字段,我們的寫(xiě)法如下:

  1. if(Student!=null&&Student.getScore()!=null&&Student.getScore().getEnglishScore()!=null){} 

這是一個(gè)很多層的判斷,而對(duì)于kotlin我們只需要寫(xiě)Student?.score?.englishScore即可,代碼量巨減有沒(méi)有。對(duì)于kotlin的特性,有興趣的同學(xué)可以移步官網(wǎng)去詳細(xì)了解。

讓我們回歸單元測(cè)試,convert類(lèi)是一個(gè)數(shù)據(jù)判空類(lèi),它的作用是對(duì)數(shù)據(jù)進(jìn)行組裝并賦予默認(rèn)初值,因?yàn)榉?wù)端的數(shù)據(jù)不可控,作為手機(jī)端我們不能把用戶體驗(yàn)完全寄托于后端的兄弟,因?yàn)榉胚^(guò)任何一個(gè)null數(shù)據(jù)對(duì)于App都是一個(gè)Crash。所以我們的測(cè)試點(diǎn)就是,這個(gè)類(lèi)是否達(dá)到了當(dāng)數(shù)據(jù)為空的時(shí)候賦予默認(rèn)值,當(dāng)數(shù)據(jù)不為空的時(shí)候取網(wǎng)絡(luò)數(shù)據(jù)值的作用。這里選取一個(gè)比較有代表性的testTemperature為例,首先設(shè)定模擬WeatherData的值為10D,因?yàn)榫W(wǎng)絡(luò)數(shù)據(jù)有值,所以會(huì)取網(wǎng)絡(luò)數(shù)據(jù)的值即10D,通過(guò)assertEquals可以進(jìn)行斷言比對(duì)驗(yàn)證,不過(guò)有一個(gè)需要注意的是double型的斷言assertEquals(message,double1,double2)是不可用的,直接運(yùn)行的話會(huì)報(bào)測(cè)試失敗。Double的比對(duì)需要加上一個(gè)誤差值,這里給一個(gè)誤差值0.1D,再次運(yùn)行,測(cè)試條變綠。同時(shí)我們需要測(cè)試當(dāng)WeatherData為空的情況下,viewData是否被賦予了默認(rèn)值0.0。以此類(lèi)推,我們需要對(duì)每一條數(shù)據(jù)進(jìn)行校驗(yàn),并包裝成視圖數(shù)據(jù)。

  1. /** 
  2.  
  3. * Author : YangHaoyi on 2017/7/7. 
  4.  
  5. * Email  :  yanghaoyi@neusoft.com 
  6.  
  7. * Description : 
  8.  
  9. * Change : YangHaoYi on 2017/7/7. 
  10.  
  11. * Version : V 1.0 
  12.  
  13. */ 
  14.  
  15. public classWeatherDataConvertTest { 
  16.  
  17.     privateWeatherDataConvertconvert; 
  18.  
  19.     private static doubleDETAL=0.1D; 
  20.  
  21.     @Beforepublic voidsetUp(){ 
  22.  
  23.         convert=newWeatherDataConvert(); 
  24.  
  25.     } 
  26.  
  27.     @Testpublic voidtestTemperature(){ 
  28.  
  29.         WeatherData netData =newWeatherData(); 
  30.  
  31.         WeatherData.DataBean dataBean =newWeatherData.DataBean(); 
  32.  
  33.         dataBean.setTemperature(10D); 
  34.  
  35.         netData.setData(dataBean); 
  36.  
  37.         WeatherViewData viewData =convert.convertData(netData); 
  38.  
  39.         //斷言double不可以用assertEquals(message,double1,double2)//需要改用下面的方法,DETAL為誤差值assertEquals(viewData.getTemperature(),10D,DETAL); 
  40.  
  41.     } 
  42.  
  43.     @Testpublic voidtestTemperatureNull(){ 
  44.  
  45.         WeatherData netData =newWeatherData(); 
  46.  
  47.         WeatherData.DataBean dataBean =newWeatherData.DataBean(); 
  48.  
  49.         netData.setData(dataBean); 
  50.  
  51.         WeatherViewData viewData =convert.convertData(netData); 
  52.  
  53.         //斷言double不可以用assertEquals(message,double1,double2)//需要改用下面的方法,DETAL為誤差值assertEquals(viewData.getTemperature(),0D,DETAL); 
  54.  
  55.     } 
  56.  

Convert類(lèi)的順利執(zhí)行標(biāo)志著Model層的測(cè)試圓滿結(jié)束,下面讓我們來(lái)看一看MVP架構(gòu)下的第二順位View層的測(cè)試,如果我們不借助UI測(cè)試框架直接運(yùn)行UI測(cè)試是無(wú)法得到預(yù)期的驗(yàn)證的,因?yàn)槲覀冎粫?huì)得到一個(gè)運(yùn)行時(shí)異常。可是我們?cè)跇?gòu)建工程之前已經(jīng)下載了對(duì)應(yīng)版本的安卓Sdk,為什么還是會(huì)拋出異常呢?在真機(jī)或是模擬器上面為什么不會(huì)呢?是不是IDE只為我們提供了工程的開(kāi)發(fā)與編譯環(huán)境,并沒(méi)有提供工程的運(yùn)行環(huán)境呢?引用Linus Torvalds的那句經(jīng)典的RTFSC,讓我們通過(guò)源碼來(lái)一點(diǎn)點(diǎn)驗(yàn)證我們的猜想。首先我們找到SDK對(duì)應(yīng)的android.jar文件,然后隨便找個(gè)工程add as library,以我們最常用的Activity為例,源碼如下:

  1. public WindowManager getWindowManager() { 
  2.  
  3.     throw newRuntimeException("Stub!"); 
  4.  
  5.  
  6. public Window getWindow() { 
  7.  
  8.     throw newRuntimeException("Stub!"); 
  9.  
  10.  
  11. public LoaderManager getLoaderManager() { 
  12.  
  13.     throw newRuntimeException("Stub!"); 
  14.  
  15.  
  16. public View getCurrentFocus() { 
  17.  
  18.     throw newRuntimeException("Stub!"); 
  19.  
  20.  
  21. protected void onCreate(BundlesavedInstanceState) { 
  22.  
  23.     throw new RuntimeException("Stub!"); 
  24.  
  25.  
  26. public void onCreate(BundlesavedInstanceState, PersistableBundle persistentState) { 
  27.  
  28.     throw newRuntimeException("Stub!"); 
  29.  

我們可以清除的看到所有的方法都不約而同的拋出了RuntimeException("Stub!"),這也就是我們的測(cè)試case無(wú)法進(jìn)行的原因。為了應(yīng)對(duì)UI單元測(cè)試難以推進(jìn)的現(xiàn)狀,谷歌推出了一套名為Espresso的UI單元測(cè)試框架,由于是官方的框架,所以在工程的運(yùn)行以及相關(guān)資料的跟進(jìn)都做的比較完善。然而Espresso的短板也非常明顯,Espresso必須借助于安卓模擬器或是真機(jī)環(huán)境才能夠運(yùn)行,也正是因?yàn)樾枰诎沧吭O(shè)備上運(yùn)行,Espresso的運(yùn)行速度非常緩慢,使之與Jenkins相結(jié)合進(jìn)行自動(dòng)化構(gòu)建更是難上加難。這不禁讓我陷入沉思,如果UI單元測(cè)試需要如此的大費(fèi)周章,那是否還有測(cè)下去的必要?不過(guò)很快迭代的bug統(tǒng)計(jì)就打消了我放棄UI只做邏輯測(cè)試的念頭。我們手機(jī)組在迭代過(guò)程中的UI與邏輯bug比基本可以達(dá)到5比1,也就是說(shuō)有絕大多數(shù)問(wèn)題產(chǎn)生在了視圖層,單元測(cè)試的目的是減少bug產(chǎn)生,而目前UI就是我們***的痛點(diǎn),UI單元測(cè)試勢(shì)在必行。經(jīng)過(guò)不斷的資源搜索,最終我到了一個(gè)可以不借助安卓設(shè)備的UI測(cè)試框架Robolectric,它的設(shè)計(jì)思路是通過(guò)實(shí)現(xiàn)一套JVM能運(yùn)行Android代碼,從而做到脫離Android環(huán)境進(jìn)行測(cè)試。由于robolectric需要從oss.sonatype.org下載一些必要的依賴包,但是oss.sonatype.org是國(guó)外的網(wǎng)站,下載速度比較緩慢。這里需要修改整個(gè)工程的build.gradle文件,修改mavenCentral()為阿里云{"http://maven.aliyun.com/nexus/content/groups/public/"} 的代理。

Robolectric的依賴為:

  1. testCompile'org.robolectric:robolectric:3.3.2' 

運(yùn)行Robolectric需要首先對(duì)測(cè)試類(lèi)進(jìn)行配置,如下:

  1. @RunWith(MyRobolectricTestRunner.class)@Config(constants= BuildConfig.class,sdk=24) 

MyRobolectricTestRunner為自定義的指向阿里云的配置文件,BuildConfig為當(dāng)前model的BuildConfig文件,sdk為使用的sdk版本,之所以指定sdk版本是因?yàn)镽obolectric需要下載對(duì)應(yīng)sdk的鏡像資源,指定版本就會(huì)使用本地已經(jīng)下載好的sdk資源。***次運(yùn)行測(cè)試的時(shí)候會(huì)自動(dòng)到阿里云去下載相關(guān)文件,然后會(huì)在系統(tǒng)的C盤(pán)下生成一個(gè).m2文件夾,如果依舊下載緩慢,可直接拷貝.m2文件夾到自己電腦的相對(duì)目錄下直接使用。Robolectric幾乎可以測(cè)試一切安卓方法,使用也是非常簡(jiǎn)單。例如:

  1. @Beforepublic voidsetUp() {  
  2.   activity= Robolectric.setupActivity(WeatherActivity.class); 

實(shí)現(xiàn)的便是創(chuàng)建一個(gè)Activity,一行代碼即可模擬activity的創(chuàng)建與運(yùn)行。一行代碼就解決了一直困擾我們對(duì)于android環(huán)境無(wú)法獲取的苦惱。有了Activity對(duì)象,瞬間覺(jué)得可以解決所有問(wèn)題。例如測(cè)試頁(yè)面的跳轉(zhuǎn):

  1. @Testpublic voidtestToHelpCenter(){ 
  2.  
  3.     view.toHelpCenter(); 
  4.  
  5.     //設(shè)置期待IntentIntent expectedIntent =newIntent(activity,WeatherHelpCenterActivity.class);//獲取實(shí)際IntentIntent actualIntent = ShadowApplication.getInstance().getNextStartedActivity();//通過(guò)Assert驗(yàn)證Assert.assertEquals(expectedIntent.getComponent(),actualIntent.getComponent()); 
  6.  

設(shè)置好當(dāng)前頁(yè)面與跳轉(zhuǎn)頁(yè)面,Robolectric就能夠幫助我們模擬出我們所期待的Intent,同時(shí)通過(guò)ShadowApplicaiton可以獲取到模擬運(yùn)行后的實(shí)際Intent的值,結(jié)合Junit即可完成對(duì)Intent的驗(yàn)證,進(jìn)而驗(yàn)證頁(yè)面跳轉(zhuǎn)邏輯。

TextView是我們?cè)陂_(kāi)發(fā)過(guò)程中最常用也是最容易出錯(cuò)的一個(gè)UI組件,尤其是團(tuán)隊(duì)的設(shè)計(jì)師是一個(gè)非常把不同地方的文案設(shè)計(jì)得非常想象而又有著細(xì)微差別的時(shí)候,我們非常容易多打或是少打一個(gè)字,又或是錯(cuò)別或是形近字。為了保證產(chǎn)品質(zhì)量,我們不得不一遍又一遍的比對(duì)UI稿件,錙銖必較,逐字觀察,簡(jiǎn)直苦不堪言。所謂程序即生活,難道我們生活中就沒(méi)有這種校驗(yàn)文字的困擾么?生活中我們又都是怎么解決的呢?記得許多年前時(shí)不時(shí)會(huì)看到有人去ATM轉(zhuǎn)賬轉(zhuǎn)錯(cuò)的新聞,今年來(lái)倒是很少有這樣的新聞了,原因就在于銀行對(duì)于銀行卡號(hào)作了二次校驗(yàn)。對(duì)于TextView的測(cè)試也是利用了二次校驗(yàn)的方法,***次文字使用業(yè)務(wù)代碼,第二次代碼使用測(cè)試代碼進(jìn)行校驗(yàn),如果兩次不一致則證明文字存在問(wèn)題。這樣就可以有效的避免了靠肉眼比對(duì)的不確定性,讓程序去驗(yàn)證程序。

  1. @Testpublic voidtestShowTemperature(){ 
  2.  
  3.     //模擬視圖數(shù)據(jù)WeatherViewData viewData =newWeatherViewData(); 
  4.  
  5.     viewData.setTemperature(23.1D); 
  6.  
  7.     view.updateCache(viewData); 
  8.  
  9.     //執(zhí)行待測(cè)函數(shù)view.showTemperature();//通過(guò)Id獲得view實(shí)體TextView tvTemperature = (TextView)activity.findViewById(R.id.tvTemperature); 
  10.  
  11.     String text = tvTemperature.getText().toString(); 
  12.  
  13.     //驗(yàn)證文字顯示assertEquals("驗(yàn)證溫度",text,"23.1"); 
  14.  

首先通過(guò)view.showTemperature();調(diào)用執(zhí)行函數(shù),在通過(guò)Id找到對(duì)應(yīng)的TextView組件,通過(guò)getText獲取TextView的顯示文字,再通過(guò)Junit的aseertEquals進(jìn)行字符串驗(yàn)證即可。如果發(fā)生比對(duì)失敗,通過(guò)下方的Log提示click to see difference即可準(zhǔn)確的看到差異點(diǎn)。

Robolectric對(duì)于提示Tost的測(cè)試也是非常的簡(jiǎn)單,只需要:

  1. @Testpublic voidtestShowDataError(){ 
  2.  
  3.     view.showDataError(); 
  4.  
  5.     assertEquals("數(shù)據(jù)轉(zhuǎn)換異常",ShadowToast.getTextOfLatestToast()); 
  6.  

測(cè)試Resource中的顏色:

  1. @Testpublic voidtestInitTitle(){ 
  2.  
  3.     TextView tvTitle = (TextView)activity.findViewById(R.id.tvTitle); 
  4.  
  5.     view.initTitle(); 
  6.  
  7.     String title = tvTitle.getText().toString(); 
  8.  
  9.     assertEquals("驗(yàn)證標(biāo)題初始化",title,"幫助中心"); 
  10.  
  11.     Application application = RuntimeEnvironment.application; 
  12.  
  13.     ColorStateList color = ColorStateList.valueOf(application.getResources().getColor(R.color.colorWhite)); 
  14.  
  15.     assertEquals("驗(yàn)證顏色",color,tvTitle.getTextColors()); 
  16.  

測(cè)試Dialog:

  1. @Testpublic voidtestShowTelDialog(){ 
  2.  
  3.     view.showTelDialog(); 
  4.  
  5.     //因?yàn)樘崾究?nbsp;dialog 在 view 中屬于私有變量,不需要對(duì)外暴露方法,如果為了測(cè)試而寫(xiě)一個(gè)get set 方法似乎太過(guò)牽強(qiáng)//所以采用 Java 反射的方法獲取dialog對(duì)象try{// /通過(guò)類(lèi)的字節(jié)碼得到該類(lèi)中聲明的所有屬性,無(wú)論私有或公有Field field = WeatherHelpCenterImpl.class.getDeclaredField("telDialog");// 設(shè)置訪問(wèn)權(quán)限(這點(diǎn)對(duì)于有過(guò)android開(kāi)發(fā)經(jīng)驗(yàn)的可以說(shuō)很熟悉)field.setAccessible(true);// 得到私有的變量值Object dialog = field.get(view); 
  6.  
  7.     TConfirmDialog telDialog = (TConfirmDialog) dialog; 
  8.  
  9.     //獲取到Dialog對(duì)象之后,再通過(guò)反射獲取Dialog中TextView對(duì)象Field fieldDialog = TConfirmDialog.class.getDeclaredField("tvTitle");// 設(shè)置訪問(wèn)權(quán)限fieldDialog.setAccessible(true);//獲取telDialog中的TextView對(duì)象Object title = fieldDialog.get(telDialog); 
  10.  
  11.     TextView tvTitle = (TextView) title; 
  12.  
  13.     //通過(guò)assert方法驗(yàn)證標(biāo)題assertEquals("驗(yàn)證標(biāo)題",tvTitle.getText().toString(),"客服電話");//獲取到Dialog對(duì)象之后,再通過(guò)反射獲取Dialog中TextView對(duì)象fieldDialog = TConfirmDialog.class.getDeclaredField("tvConfirm");//獲取telDialog中的TextView對(duì)象Object confirm = fieldDialog.get(telDialog); 
  14.  
  15.     TextView tvConfirm = (TextView) confirm; 
  16.  
  17.     //通過(guò)assert方法驗(yàn)證標(biāo)題assertEquals("驗(yàn)證確定按鈕",tvConfirm.getText().toString(),"撥打電話");//獲取到Dialog對(duì)象之后,再通過(guò)反射獲取Dialog中TextView對(duì)象fieldDialog = TConfirmDialog.class.getDeclaredField("tvCancel");//獲取telDialog中的TextView對(duì)象Object cancel = fieldDialog.get(telDialog); 
  18.  
  19.     TextView tvCancel = (TextView) cancel; 
  20.  
  21.     //通過(guò)assert方法驗(yàn)證標(biāo)題assertEquals("驗(yàn)證取消按鈕",tvCancel.getText().toString(),"取消"); 
  22.  
  23.  
  24. catch(Exception e) { 
  25.  
  26.     //error} 
  27.  

Dialog的測(cè)試點(diǎn)需要包括Dialog的顯示與隱藏,Dialog的提示文字與按鈕的文字顯示,因?yàn)楹芏嗍撬接凶兞浚赃@里用到了一些Java反射來(lái)幫助獲取對(duì)象。

目前為止,我們已經(jīng)完成了對(duì)Model層與View層的測(cè)試,MVP三兄弟只剩下P層還沒(méi)有測(cè)試,下面我們就來(lái)看看P層該如何測(cè)試。P層作為M層與V層的紐帶,起到了隔離視圖與數(shù)據(jù)直接交互的作用。因?yàn)镻層持有的只是V的接口,所以P層也可以抽離成簡(jiǎn)單的純Java測(cè)試。讓我們先來(lái)看看P層的測(cè)試代碼:

  1. /** 
  2.  
  3. * Created by YangHaoyi on 2017/7/8. 
  4.  
  5. * Email  : yanghaoyi@neusoft.com 
  6.  
  7. * Description : 
  8.  
  9. * Version : 
  10.  
  11. */ 
  12.  
  13. public classWeatherPresenterTest { 
  14.  
  15.     privateWeatherPresenterpresenter; 
  16.  
  17.     privateIWeatherViewview; 
  18.  
  19.     privateWeatherControlcontrol; 
  20.  
  21.     privateWeatherModelweatherModel; 
  22.  
  23.     privateWeatherRequestListenerlistener; 
  24.  
  25.     @Beforepublic voidsetUp(){ 
  26.  
  27.         view=mock(IWeatherView.class); 
  28.  
  29.         control=mock(WeatherControl.class); 
  30.  
  31.         weatherModel=mock(WeatherModel.class); 
  32.  
  33.         listener=mock(WeatherRequestListener.class); 
  34.  
  35.         presenter=newWeatherPresenter(view); 
  36.  
  37.         presenter.updateWeatherModel(weatherModel); 
  38.  
  39.         presenter.updateControl(control); 
  40.  
  41.         presenter.updateListener(listener); 
  42.  
  43.     } 
  44.  
  45.     @Testpublic voidtestRequest(){ 
  46.  
  47.         presenter.request(); 
  48.  
  49.         verify(weatherModel).request(listener,view.getLocationCity()); 
  50.  
  51.     } 
  52.  
  53.     @Testpublic voidtestCancelRequest(){ 
  54.  
  55.         presenter.cancelRequest(); 
  56.  
  57.         verify(weatherModel).cancelRequest(); 
  58.  
  59.     } 
  60.  
  61.     @Testpublic voidtestShowHourTemperature(){ 
  62.  
  63.         presenter.showHourTemperature(); 
  64.  
  65.         verify(control).buttonWasPressed(WeatherControl.TEMPERATURE); 
  66.  
  67.     } 
  68.  
  69.     @Testpublic voidtestShowPrecipitation(){ 
  70.  
  71.         presenter.showPrecipitation(); 
  72.  
  73.         verify(control).buttonWasPressed(WeatherControl.PRECIPITATION); 
  74.  
  75.     } 
  76.  
  77.     @Testpublic voidtestShowWindPower(){ 
  78.  
  79.         presenter.showWindPower(); 
  80.  
  81.         verify(control).buttonWasPressed(WeatherControl.WINDPOWER); 
  82.  
  83.     } 
  84.  
  85.     @Testpublic voidtestToHelpCenter(){ 
  86.  
  87.         presenter.toHelpCenter(); 
  88.  
  89.         verify(view).toHelpCenter(); 
  90.  
  91.     } 
  92.  

由于這只是一個(gè)示例Demo,沒(méi)有過(guò)多的業(yè)務(wù)邏輯,結(jié)合了幾個(gè)簡(jiǎn)單的設(shè)計(jì)模式,Presenter的代碼變成了絕大多數(shù)的順序執(zhí)行,通過(guò)Mockito的verify即可完成驗(yàn)證。這里需要說(shuō)明一下的是之所以結(jié)合設(shè)計(jì)模式是因?yàn)閱卧獪y(cè)試的原則是每一個(gè)條件分支都需要有一條測(cè)試Case做保證,對(duì)于多分支甚至是多嵌套分支就會(huì)比較繁瑣,需要寫(xiě)大量的重復(fù)代碼,同時(shí)也增大了漏測(cè)的幾率,適當(dāng)?shù)奶砑釉O(shè)計(jì)模式可以很好的彌補(bǔ)這一點(diǎn),將嵌套條件判斷測(cè)底刪除,極大程度減少甚至刪除條件判斷。經(jīng)過(guò)完善代碼后的單元測(cè)試,測(cè)試的只是一些簡(jiǎn)單的if/else單分支判斷。驗(yàn)證方法與Model層的測(cè)試方法大同小異,借助Junit與Mockito我們可以輕易的實(shí)現(xiàn)Presenter層的測(cè)試。

寫(xiě)在后面,很多朋友對(duì)單元測(cè)試都是抱著一種排斥的態(tài)度,覺(jué)得寫(xiě)單元測(cè)試是在浪費(fèi)時(shí)間。其實(shí)不然,如果你把代碼調(diào)試,bug修復(fù)與回歸測(cè)試的時(shí)間也算人進(jìn)去的話,你就會(huì)發(fā)現(xiàn),單元測(cè)試其實(shí)能夠幫助我們節(jié)約大量的時(shí)間。單元測(cè)試的編寫(xiě)要本著驗(yàn)證問(wèn)題的心態(tài)就編碼,切不可以完成任務(wù)指標(biāo)的心態(tài)去編碼,覺(jué)得只是Leader安排的指標(biāo)。很多時(shí)候一個(gè)有經(jīng)驗(yàn)的前人安排你去做某件事的時(shí)候,并不是想讓你完成什么,只是以一個(gè)過(guò)來(lái)人的角度告訴你終南捷徑,東西就在你眼前,誰(shuí)把話聽(tīng)進(jìn)去了,誰(shuí)就得到了。

 

Jacoco代碼覆蓋率

 

責(zé)任編輯:龐桂玉 來(lái)源: 安卓巴士Android開(kāi)發(fā)者門(mén)戶
相關(guān)推薦

2009-10-12 14:06:05

U盤(pán)中毒

2023-10-12 10:22:14

JavaScripThis

2009-12-03 09:00:18

PHP分頁(yè)函數(shù)

2018-04-08 09:07:58

2011-08-11 13:11:24

準(zhǔn)入控制

2023-10-25 14:47:08

架構(gòu)設(shè)計(jì)人工智能

2023-10-18 10:42:44

WOT大會(huì)架構(gòu)架構(gòu)演進(jìn)

2021-03-07 08:30:17

Github報(bào)錯(cuò)下載資源庫(kù)

2021-12-07 07:58:33

個(gè)人圖床工具

2016-10-18 13:58:15

2022-01-17 09:58:29

自動(dòng)化訪問(wèn)權(quán)限CIO

2017-01-14 23:42:49

單元測(cè)試框架軟件測(cè)試

2022-08-02 08:07:24

單元測(cè)試代碼重構(gòu)

2010-04-22 12:07:36

lvs負(fù)載均衡

2020-08-18 08:10:02

單元測(cè)試Java

2022-02-14 22:22:30

單元測(cè)試Junit5

2023-11-13 10:55:09

MySQL數(shù)據(jù)庫(kù)

2017-01-16 12:12:29

單元測(cè)試JUnit

2017-01-14 23:26:17

單元測(cè)試JUnit測(cè)試

2011-05-16 16:52:09

單元測(cè)試徹底測(cè)試
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)

主站蜘蛛池模板: 精品久久99 | 狠狠爱免费视频 | 亚洲一区二区黄 | 欧美一区二区在线观看 | 亚洲国产欧美国产综合一区 | 欧美4p| 亚洲精久久 | 黄色片a级 | www.色五月.com| 国产精品一区二区免费 | 精品国产区 | 午夜精品网站 | 一级片av | 国产 91 视频 | 污片在线免费观看 | 激情的网站 | 欧美精品在线播放 | 婷婷丁香激情 | 黄色日本视频 | 日韩伦理电影免费在线观看 | 国产精品18久久久久久白浆动漫 | 欧美一区免费 | 美女国产一区 | 欧美一区成人 | 国产成人精品一区二区在线 | 久久最新网址 | 欧美精品一区在线观看 | 久久偷人| 国产在线观看一区二区 | 久久国产精品精品国产色婷婷 | 91av视频在线观看 | 久久激情av | 红桃视频一区二区三区免费 | 国产成人精品一区二区三区在线 | 2023亚洲天堂 | 国产精品视频久久久 | www.色.com | 久久国 | 精品国产乱码久久久久久a丨 | 91porn国产成人福利 | 成人亚洲性情网站www在线观看 |