精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

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

移動(dòng)開發(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)而頻繁的開關(guān)WiFi開關(guān)?或者你是否還在為一個(gè)switch判斷而頻繁的使用debug斷點(diǎn)setValue來觀測(cè)代碼的邏輯判斷情況?又或者你是否還在為了校驗(yàn)?zāi)硞€(gè)UI文案的正確性而反復(fù)的比對(duì)UI稿?可能你會(huì)反問,難道寫完代碼自測(cè)也有錯(cuò)?當(dāng)然不是,自測(cè)是一個(gè)良好的習(xí)慣,不過作為一名工程師,你要做的不應(yīng)該只是看看點(diǎn)點(diǎn)的黑盒測(cè)試,而是應(yīng)該設(shè)計(jì)出一套能夠讓代碼測(cè)試代碼,一勞永逸的測(cè)試工程。

正文

首先我們從Model層開始,通過具體代碼來詳盡說明一下一個(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è)沒有安卓依賴的Model類,跑完全部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","沈陽"); 
  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.  

首先通過@Mock注解對(duì)需要mock的對(duì)象進(jìn)行初始化,然后我們需要對(duì)測(cè)試類進(jìn)行測(cè)試case分析,WeatherModelmode類是一個(gè)網(wǎng)絡(luò)請(qǐng)求數(shù)據(jù)model,所以這個(gè)model類的核心是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類中還有一個(gè)觀察者解綁函數(shù),所以測(cè)試case也需要包含解綁函數(shù)處理邏輯測(cè)試這一項(xiàng)。通過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,"沈陽"); 
  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,"沈陽"); 
  24.  
  25.     } 
  26.  
  27.     catch(Exception e) { 
  28.  
  29.         //reflect error} 
  30.  
  31.     } 

對(duì)于有參數(shù)的Api,***步就是驗(yàn)證傳參??赡苣銜?huì)覺得大材小用,但眾多的血淋淋的慘案告訴我們?cè)绞羌?xì)小的東西越容易產(chǎn)生問題,而單元測(cè)試就是幫助我們將細(xì)小的問題解決在編碼時(shí)期而不對(duì)外暴露。要驗(yàn)證參數(shù)的正確性,首先我們需要要驗(yàn)證向queryMap中put的時(shí)候是否正確。對(duì)于queryMap,我們需要驗(yàn)證K-V鍵值對(duì)的正確性,還是那句防微杜漸,因?yàn)閝ueryMap是一個(gè)private變量,在正常情況下我們無法獲取到它的值,而為這個(gè)變量加一個(gè)對(duì)業(yè)務(wù)毫無用處的get/set方法就顯得太刻意了,我們的目的是為了解決讓代碼更健壯,bug更少,而不是為了測(cè)試而測(cè)試。拿不到queryMap參數(shù)測(cè)試還怎么進(jìn)行?難道單元測(cè)試也要從入門到放棄?要其實(shí)很多事情都是這樣,當(dāng)你覺得某個(gè)問題完全沒有辦法解決的時(shí)候,一定是你考慮的不夠周全。queryMap對(duì)象的值我們可以通過Java反射獲得。反射的原理在這里我就不為大家闡述了,在testParams方法中,我們首先通過getDeclaredField獲取了queryMap對(duì)象,然后我們需要獲得到put的key。key的獲得使我們陷入了第二個(gè)難題,可能你會(huì)說,這有什么難的,繼續(xù)反射啊,可這個(gè)key是一個(gè)private static變量,通過正常的反射是無法拿到key的,最多會(huì)拿到一個(gè)異常。還是那句,不要放棄尋找解決方案,最終我們發(fā)現(xiàn)只要設(shè)置下虛擬機(jī)不去檢測(cè)私有屬性,即可完成對(duì)private static變量的獲取。不要覺得只是很小的一個(gè)參數(shù),這么勞師動(dòng)眾不值得,據(jù)不完全統(tǒng)計(jì),每天因?yàn)榻涌趉ey值多寫或是寫錯(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,"沈陽"); 
  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ù)傳遞的前提下,我們接下來需要對(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í)行。不過這個(gè)聲明必須在方法執(zhí)行之前,即Mockito.when必須比model.request(listener,"沈陽");先執(zhí)行才會(huì)生效。Junit提供了豐富的assert斷言機(jī)制,借助assert我們可以實(shí)現(xiàn)多種情況的測(cè)試,然而對(duì)于沒有明確返回值的void方法,assert就顯得有些無能為力,因?yàn)樗鼰o法找到一個(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ù),通過assert斷言,我們可以驗(yàn)證成功情況下數(shù)據(jù)是否正確。數(shù)據(jù)成功情況下,我們有一個(gè)網(wǎng)絡(luò)數(shù)據(jù)向視圖數(shù)據(jù)轉(zhuǎn)換的過程,這個(gè)轉(zhuǎn)換方法是在convert類中執(zhí)行的操作,因?yàn)槲覀冏龅氖菃卧獪y(cè)試而非集成測(cè)試,所以基于WeatherModel這個(gè)測(cè)試類,我們只需要驗(yàn)證到convertData()這個(gè)函數(shù)是否正確得到了調(diào)用即可,數(shù)據(jù)轉(zhuǎn)換的內(nèi)容由Convert類的單元測(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,"沈陽"); 
  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í)際開發(fā)過程中,服務(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()類似,只是我們這次的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,"沈陽"); 
  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類的時(shí)候也要將非常態(tài)考慮在內(nèi),對(duì)接口的異常情況進(jìn)行處理,有時(shí)候我們需要自己創(chuàng)造一些異常來驗(yàn)證我們代碼的健壯程度。同樣的,我們的測(cè)試類也需要有一個(gè)專門的方法來保證異常態(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類中***一個(gè)case是testCancelRequest()它的作用是,在合適的時(shí)候解綁request,我們的網(wǎng)絡(luò)請(qǐng)求是異步的,也就是說當(dāng)我們調(diào)用請(qǐng)求的activity或是fragment destroy的時(shí)候,如果我們沒有解除綁定,是存在內(nèi)存泄漏風(fēng)險(xiǎn)的。當(dāng)然,我們能想到的問題,Rxjava的維護(hù)者們也一定想到了,Subscription就是方便我們?cè)谏芷诮Y(jié)束的時(shí)候?qū)x解綁。驗(yàn)證方法很簡(jiǎn)單,還是通過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è)試類前面的運(yùn)行按鈕,可以看到所有測(cè)試類運(yùn)行的情況,綠色代表成功,紅色代表存在問題,可以通過下方的Log日志查看引起測(cè)試失敗的問題點(diǎn)進(jìn)行改正,借助Jacoco統(tǒng)計(jì)工具可以看到單元測(cè)試覆蓋率的情況。之所以選擇使用Jacoco而不是IDE自帶的Coverage是因?yàn)樵跍y(cè)試&條件分支的情況下Coverage存在漏洞,導(dǎo)致沒有達(dá)到全覆蓋的測(cè)試顯示已覆蓋完全。Jacoco的AndroidStudio集成網(wǎng)絡(luò)資源并不多,集成方法不是存在潛在漏洞就是過于繁瑣。經(jīng)過兩天的不斷搜索,終于發(fā)現(xiàn)了一個(gè)史上最簡(jiǎn)單集成方法,只需要在主工程的gradle文件中添加一個(gè)Jacoco插件,gradle就會(huì)生成一個(gè)Jacoco Task,雙擊運(yùn)行即可生成一份Html覆蓋率報(bào)告。運(yùn)行我們的model測(cè)試類,從jacoco生成的html可以看到,我們的model已經(jīng)達(dá)到了100%的全覆蓋。既然如此,我們是不是就可以認(rèn)為MVP的M層已經(jīng)ok了呢?等等,我們好像遺漏了點(diǎn)什么,沒錯(cuò),onNext情況下的數(shù)據(jù)轉(zhuǎn)換類還沒有測(cè)試,下面我們來對(duì)convert類進(jìn)行一下測(cè)試。

首先們來看看convert類代碼:

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

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

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

讓我們回歸單元測(cè)試,convert類是一個(gè)數(shù)據(jù)判空類,它的作用是對(duì)數(shù)據(jù)進(jìn)行組裝并賦予默認(rèn)初值,因?yàn)榉?wù)端的數(shù)據(jù)不可控,作為手機(jī)端我們不能把用戶體驗(yàn)完全寄托于后端的兄弟,因?yàn)榉胚^任何一個(gè)null數(shù)據(jù)對(duì)于App都是一個(gè)Crash。所以我們的測(cè)試點(diǎn)就是,這個(gè)類是否達(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,通過assertEquals可以進(jìn)行斷言比對(duì)驗(yàn)證,不過有一個(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。以此類推,我們需要對(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類的順利執(zhí)行標(biāo)志著Model層的測(cè)試圓滿結(jié)束,下面讓我們來看一看MVP架構(gòu)下的第二順位View層的測(cè)試,如果我們不借助UI測(cè)試框架直接運(yùn)行UI測(cè)試是無法得到預(yù)期的驗(yàn)證的,因?yàn)槲覀冎粫?huì)得到一個(gè)運(yùn)行時(shí)異常??墒俏覀?cè)跇?gòu)建工程之前已經(jīng)下載了對(duì)應(yīng)版本的安卓Sdk,為什么還是會(huì)拋出異常呢?在真機(jī)或是模擬器上面為什么不會(huì)呢?是不是IDE只為我們提供了工程的開發(fā)與編譯環(huán)境,并沒有提供工程的運(yùn)行環(huán)境呢?引用Linus Torvalds的那句經(jīng)典的RTFSC,讓我們通過源碼來一點(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無法進(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è)下去的必要?不過很快迭代的bug統(tǒng)計(jì)就打消了我放棄UI只做邏輯測(cè)試的念頭。我們手機(jī)組在迭代過程中的UI與邏輯bug比基本可以達(dá)到5比1,也就是說有絕大多數(shù)問題產(chǎn)生在了視圖層,單元測(cè)試的目的是減少bug產(chǎn)生,而目前UI就是我們***的痛點(diǎn),UI單元測(cè)試勢(shì)在必行。經(jīng)過不斷的資源搜索,最終我到了一個(gè)可以不借助安卓設(shè)備的UI測(cè)試框架Robolectric,它的設(shè)計(jì)思路是通過實(shí)現(xiàn)一套JVM能運(yùn)行Android代碼,從而做到脫離Android環(huán)境進(jìn)行測(cè)試。由于robolectric需要從oss.sonatype.org下載一些必要的依賴包,但是oss.sonatype.org是國外的網(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è)試類進(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盤下生成一個(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)境無法獲取的苦惱。有了Activity對(duì)象,瞬間覺得可以解決所有問題。例如測(cè)試頁面的跳轉(zhuǎn):

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

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

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

首先通過view.showTemperature();調(diào)用執(zhí)行函數(shù),在通過Id找到對(duì)應(yīng)的TextView組件,通過getText獲取TextView的顯示文字,再通過Junit的aseertEquals進(jìn)行字符串驗(yàn)證即可。如果發(fā)生比對(duì)失敗,通過下方的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è)試而寫一個(gè)get set 方法似乎太過牽強(qiáng)//所以采用 Java 反射的方法獲取dialog對(duì)象try{// /通過類的字節(jié)碼得到該類中聲明的所有屬性,無論私有或公有Field field = WeatherHelpCenterImpl.class.getDeclaredField("telDialog");// 設(shè)置訪問權(quán)限(這點(diǎn)對(duì)于有過android開發(fā)經(jīng)驗(yàn)的可以說很熟悉)field.setAccessible(true);// 得到私有的變量值Object dialog = field.get(view); 
  6.  
  7.     TConfirmDialog telDialog = (TConfirmDialog) dialog; 
  8.  
  9.     //獲取到Dialog對(duì)象之后,再通過反射獲取Dialog中TextView對(duì)象Field fieldDialog = TConfirmDialog.class.getDeclaredField("tvTitle");// 設(shè)置訪問權(quán)限fieldDialog.setAccessible(true);//獲取telDialog中的TextView對(duì)象Object title = fieldDialog.get(telDialog); 
  10.  
  11.     TextView tvTitle = (TextView) title; 
  12.  
  13.     //通過assert方法驗(yàn)證標(biāo)題assertEquals("驗(yàn)證標(biāo)題",tvTitle.getText().toString(),"客服電話");//獲取到Dialog對(duì)象之后,再通過反射獲取Dialog中TextView對(duì)象fieldDialog = TConfirmDialog.class.getDeclaredField("tvConfirm");//獲取telDialog中的TextView對(duì)象Object confirm = fieldDialog.get(telDialog); 
  14.  
  15.     TextView tvConfirm = (TextView) confirm; 
  16.  
  17.     //通過assert方法驗(yàn)證標(biāo)題assertEquals("驗(yàn)證確定按鈕",tvConfirm.getText().toString(),"撥打電話");//獲取到Dialog對(duì)象之后,再通過反射獲取Dialog中TextView對(duì)象fieldDialog = TConfirmDialog.class.getDeclaredField("tvCancel");//獲取telDialog中的TextView對(duì)象Object cancel = fieldDialog.get(telDialog); 
  18.  
  19.     TextView tvCancel = (TextView) cancel; 
  20.  
  21.     //通過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反射來幫助獲取對(duì)象。

目前為止,我們已經(jīng)完成了對(duì)Model層與View層的測(cè)試,MVP三兄弟只剩下P層還沒有測(cè)試,下面我們就來看看P層該如何測(cè)試。P層作為M層與V層的紐帶,起到了隔離視圖與數(shù)據(jù)直接交互的作用。因?yàn)镻層持有的只是V的接口,所以P層也可以抽離成簡(jiǎn)單的純Java測(cè)試。讓我們先來看看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,沒有過多的業(yè)務(wù)邏輯,結(jié)合了幾個(gè)簡(jiǎn)單的設(shè)計(jì)模式,Presenter的代碼變成了絕大多數(shù)的順序執(zhí)行,通過Mockito的verify即可完成驗(yàn)證。這里需要說明一下的是之所以結(jié)合設(shè)計(jì)模式是因?yàn)閱卧獪y(cè)試的原則是每一個(gè)條件分支都需要有一條測(cè)試Case做保證,對(duì)于多分支甚至是多嵌套分支就會(huì)比較繁瑣,需要寫大量的重復(fù)代碼,同時(shí)也增大了漏測(cè)的幾率,適當(dāng)?shù)奶砑釉O(shè)計(jì)模式可以很好的彌補(bǔ)這一點(diǎn),將嵌套條件判斷測(cè)底刪除,極大程度減少甚至刪除條件判斷。經(jīng)過完善代碼后的單元測(cè)試,測(cè)試的只是一些簡(jiǎn)單的if/else單分支判斷。驗(yàn)證方法與Model層的測(cè)試方法大同小異,借助Junit與Mockito我們可以輕易的實(shí)現(xiàn)Presenter層的測(cè)試。

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

 

Jacoco代碼覆蓋率

 

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

2009-10-12 14:06:05

U盤中毒

2023-10-12 10:22:14

JavaScripThis

2009-12-03 09:00:18

PHP分頁函數(shù)

2018-04-08 09:07:58

2023-10-25 14:47:08

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

2023-10-18 10:42:44

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

2011-08-11 13:11:24

準(zhǔn)入控制

2021-03-07 08:30:17

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

2021-12-07 07:58:33

個(gè)人圖床工具

2016-10-18 13:58:15

2022-01-17 09:58:29

自動(dòng)化訪問權(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ù)庫

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)

一二美女精品欧洲| 91久久久免费一区二区| 国产传媒一区二区三区| 国产精品一区二区三区四| 视频一区在线观看| 欧美精品国产精品| 免费看一级大黄情大片| 在线观看麻豆| 99免费精品在线| 国产美女久久久| 日产亚洲一区二区三区| 色爱综合网欧美| 亚洲黄色免费三级| 五月天婷婷亚洲| 波多野结衣亚洲一二三| 亚洲精品视频在线| 日本精品二区| 黄色av网址在线| 久久99久久久欧美国产| 97人洗澡人人免费公开视频碰碰碰| 亚洲高潮女人毛茸茸| jizz性欧美2| 欧美日韩成人在线| 高清一区二区视频| 爱啪啪综合导航| 亚洲欧美日韩一区| 亚洲韩国在线| 欧美男男同志| 91色婷婷久久久久合中文| 91福利入口| 国产精品一区二区av白丝下载 | 99久久综合精品| 成人免费网站在线| 中文字幕日本人妻久久久免费| 国产欧美高清| 国内精品久久久久久影视8| 欧美视频一区二区在线| 狠狠色狠狠色综合婷婷tag| 亚洲第一区中文字幕| 伊人影院在线观看视频| 国产视频一区二| 欧美老年两性高潮| 亚洲娇小娇小娇小| 日韩毛片网站| 欧美日韩国产免费一区二区 | av在线播放一区| 日韩欧美成人免费视频| 少妇av一区二区三区无码| 影音先锋在线视频| 一区二区三区四区精品在线视频 | 国产男男chinese网站| 色天下一区二区三区| 亚洲精品成人久久| 欧美黑人欧美精品刺激| 婷婷激情久久| 亚洲性生活视频在线观看| 国产毛片久久久久久久| 禁果av一区二区三区| 伊人久久男人天堂| 天美传媒免费在线观看| 99久久www免费| 久久天天躁狠狠躁老女人| 少妇被躁爽到高潮无码文| 国产精品99久久精品| 欧美另类xxx| 久久免费小视频| 亚洲伦理一区| 国产91网红主播在线观看| 欧美高清69hd| 狠狠色综合播放一区二区| 91免费欧美精品| 亚洲产国偷v产偷v自拍涩爱| 99久久精品免费精品国产| 欧美激情论坛| 色三级在线观看| 亚洲精品中文字幕乱码三区 | 久久综合九色综合欧美狠狠| 国产精品免费视频久久久| 91极品身材尤物theporn| 国产一区二区91| 国产一区二区不卡视频| 九九在线视频| 亚洲摸摸操操av| 福利视频一二区| 精品123区| 日韩欧美国产小视频| 成人免费无码大片a毛片| 精品国产123区| 久久资源免费视频| 亚洲男人的天堂在线视频| 蓝色福利精品导航| 国产久一道中文一区| 精品福利视频导航大全| 亚洲激情欧美激情| 国内外免费激情视频| 欧美成年网站| 亚洲男人天堂九九视频| 丝袜美腿小色网| 国产精品久久久一区二区| 成人国产在线视频| 欧美视频综合| 怡红院av一区二区三区| 免费激情视频在线观看| 97成人在线| 中文字幕日韩在线观看| 久草精品视频在线观看| 久久精品99国产精品| 精品国产免费久久久久久尖叫 | wwwwwxxxx日本| 欧美成人专区| 欧美大尺度激情区在线播放| 超碰在线观看91| 成人综合婷婷国产精品久久免费| 日韩亚洲欧美精品| 都市激情国产精品| 日韩欧美中文一区| 老司机精品免费视频| 一本色道久久综合| 成人精品一二区| 一级毛片视频在线| 色婷婷激情综合| 欧美夫妇交换xxx| 中文字幕av亚洲精品一部二部| 日韩免费中文字幕| 亚洲av成人无码久久精品老人 | 一区二区视频网| www激情久久| 国产精品www在线观看| 激情视频亚洲| 色七七影院综合| 麻豆成人免费视频| 91麻豆蜜桃一区二区三区| 男人添女人荫蒂免费视频| 日韩在线网址| 欧美成人性色生活仑片| 国产色片在线观看| 亚洲视频在线一区观看| 色啦啦av综合| 日韩国产欧美| 国产精品中文字幕在线| 在线视频91p| 欧美日韩精品一区二区三区四区| www.99热| 另类成人小视频在线| 亚洲欧洲一区二区| 亚洲伦理一区二区| 久久精品视频99| 国产人妻精品一区二区三| 亚洲天堂免费看| www.51色.com| 欧美a级一区| 粉嫩av四季av绯色av第一区| 久久香蕉av| 精品国产乱码久久久久久老虎| 国产无码精品视频| 91亚洲精华国产精华精华液| 亚洲国产精品久久久久婷蜜芽| 麻豆一区二区| 欧美一乱一性一交一视频| 美州a亚洲一视本频v色道| 在线中文字幕一区| 少妇的滋味中文字幕bd| 国产麻豆视频精品| 精品国产av无码一区二区三区| 日本在线中文字幕一区| 国产成人精品久久久| 91美女视频在线| 欧美一级日韩一级| 日本少妇做爰全过程毛片| 久久久亚洲精品石原莉奈| 高清一区在线观看| 婷婷六月综合| 粉嫩av一区二区三区免费观看| 中国字幕a在线看韩国电影| 国产亚洲精品va在线观看| 国产一区二区在线不卡| 亚洲一区二区三区免费视频| 在线观看av中文字幕| 销魂美女一区二区三区视频在线| 亚洲日本理论电影| a级日韩大片| 国产成人在线一区| 污视频免费在线观看| 精品视频久久久久久久| 在线观看国产黄| 亚洲国产成人av| 老熟妇一区二区| 粉嫩高潮美女一区二区三区| 日本在线视频www| 欧美激情91| 日本在线播放不卡| 亚洲经典视频| 国产精品9999| 国产天堂在线播放视频| 亚洲视屏在线播放| 精品人妻久久久久一区二区三区| 色综合夜色一区| 青青草成人免费| 国产亚洲精品aa| 又色又爽又黄18网站| 全国精品久久少妇| 国产精品无码人妻一区二区在线| 成人三级视频| 久久久综合香蕉尹人综合网| 精品国产一区二区三区2021| 国产成人精品日本亚洲| 欧洲性视频在线播放| 国产一区二区日韩| 午夜福利视频一区二区| 日韩写真欧美这视频| 无码人妻丰满熟妇奶水区码| 亚洲一区二区精品视频| 日本一级特级毛片视频| 久久精品日韩一区二区三区| 亚洲少妇18p| 国产激情精品久久久第一区二区| 福利在线一区二区三区| 一本色道久久综合| 成年人看的毛片| 五月激情综合| 亚洲欧美精品| 日本大胆欧美| 欧美日韩三区四区| 亚洲免费专区| 国产精品视频在线免费观看 | 青青草原在线免费观看| 中文天堂在线一区| 亚洲自拍偷拍图| 成人黄色a**站在线观看| 无套内谢丰满少妇中文字幕| 麻豆成人久久精品二区三区红| 亚洲高清在线免费观看| 日韩av中文字幕一区二区| 国产二区视频在线播放| 亚洲乱码久久| 国产精品后入内射日本在线观看| 99国产成+人+综合+亚洲欧美| 久久久久福利视频| 综合五月婷婷| 潘金莲一级淫片aaaaaa播放1| 婷婷综合五月| avove在线观看| 午夜国产欧美理论在线播放| av不卡在线免费观看| 国产精品7m凸凹视频分类| 一区二区三区av| 偷偷www综合久久久久久久| 一区二区三区视频| 久久久久久免费视频| 亚洲免费av网| 亚洲最新色图| 你真棒插曲来救救我在线观看| 亚洲毛片网站| 麻豆av免费在线| 日本美女视频一区二区| 欧美美女性视频| 国产九色精品成人porny| 国产黄色一区二区三区| 国产98色在线|日韩| av在线播放网址| 久久综合久久综合久久| 精品无人区无码乱码毛片国产| 国产欧美日韩另类视频免费观看 | 亚洲一区在线观看视频| 日本在线观看视频网站| 狠狠躁天天躁日日躁欧美| 无码人妻aⅴ一区二区三区有奶水| 欧美优质美女网站| 国产原创中文av| 精品免费国产二区三区| 亚洲人成色777777老人头| 尤物yw午夜国产精品视频明星| 色老头视频在线观看| 久久久久久综合网天天| 热三久草你在线| 国产日韩视频在线观看| jazzjazz国产精品久久| 欧美日韩免费观看一区| 亚洲人metart人体| 日韩欧美国产免费| 精品亚洲国内自在自线福利| 欧洲熟妇的性久久久久久| 国产午夜精品福利| 精品99久久久久成人网站免费 | 久久久久久成人精品| 日韩pacopacomama| 亚洲最大福利视频| 亚洲国产国产| 亚洲国产精品女人| 亚洲综合不卡| 日本网站在线看| 久久无码av三级| 免费三级在线观看| 日韩欧美亚洲范冰冰与中字| 99久久精品国产一区二区成人| 日韩激情av在线播放| 超碰在线网址| 国产精品久久久久久久久久久久久| 欧美电影院免费观看| 日本不卡二区| 亚洲第一精品影视| 欧美丝袜在线观看| 国产午夜精品一区二区| 久草视频在线免费看| 欧美视频第二页| 日批视频在线播放| 久久视频在线免费观看| 欧美xxxx做受欧美护士| 国产一区二区三区高清视频| 久久精品青草| 另类小说色综合| 99re这里只有精品6| 欧美极品视频在线观看| 欧美日韩一区二区在线视频| 男人的天堂在线视频| 亚州精品天堂中文字幕| 精品视频成人| 在线观看成人一级片| 视频在线观看91| 亚洲国产无码精品| 精品久久久国产精品999| 国 产 黄 色 大 片| 久久久极品av| 欧美videos粗暴| 亚洲v国产v在线观看| 美女精品网站| 中国毛片在线观看| 精品久久久久久久久中文字幕| 空姐吹箫视频大全| 久久久久久国产精品久久| 亚洲精品高潮| 欧洲精品视频在线| 国产在线精品一区二区| 三上悠亚在线观看视频| 欧美视频精品在线观看| 亚洲天天影视| 成人久久久久久久| 亚洲影视一区二区三区| 波多野结衣网页| 亚洲视频在线观看一区| 99久久婷婷国产一区二区三区| 日韩在线观看网址| 粉嫩一区二区三区在线观看| 一区二区三区四区不卡| 久久国内精品视频| 国产精品久久久精品四季影院| 欧美精品黑人性xxxx| av在线app| 国产66精品久久久久999小说| 欧美另类专区| theav精尽人亡av| 欧美性猛交xxxx富婆弯腰| 男人的天堂在线| 国产精品日日做人人爱| 五月开心六月丁香综合色啪| 亚洲av无日韩毛片久久| 一区二区在线观看免费视频播放| 成人av手机在线| 97人人模人人爽人人喊中文字| 亚洲毛片免费看| 亚洲福利精品视频| 成人免费一区二区三区在线观看| 99久久精品无免国产免费| 久久久久成人精品| 日韩有码一区| 中文字幕国内自拍| 一区二区三区在线观看网站| 视频二区在线观看| 日本精品在线视频| 欧美成人激情| 久久久久无码国产精品一区李宗瑞| 天天色 色综合| 91伦理视频在线观看| 国产精品久久7| 老牛嫩草一区二区三区日本 | 亚洲国产精品一区二区久久恐怖片 | 日本最新不卡在线| 性欧美疯狂猛交69hd| 亚洲国产成人精品一区二区| 欧美gay囗交囗交| 日本高清视频免费在线观看| 91丨porny丨最新| 亚洲图片小说视频| 久久免费视频网站| 欧美日韩国产一区二区三区不卡| 久久久精品视频国产| 欧美日韩亚洲91| 乱人伦中文视频在线| 国产综合 伊人色| 精品一区二区国语对白| 国产性xxxx高清| www.国产一区| 校花撩起jk露出白色内裤国产精品| 污污的网站免费| 欧美日韩在线视频一区二区| 黄网站在线免费看| 日本成人黄色免费看| 高清不卡一二三区| 在线免费看毛片| 日韩免费在线视频|