小男孩‘自慰网亚洲一区二区,亚洲一级在线播放毛片,亚洲中文字幕av每天更新,黄aⅴ永久免费无码,91成人午夜在线精品,色网站免费在线观看,亚洲欧洲wwwww在线观看

分享

蘑菇街支付金融Android單元測(cè)試實(shí)踐

 quasiceo 2016-07-14
作者 鄒勇 發(fā)布于 2016年5月20日 | 首屆應(yīng)用性能管理大會(huì) APMCon,聚焦國(guó)內(nèi)外性能優(yōu)化先進(jìn)技術(shù)。 

本文為『移動(dòng)前線』群在4月23日的分享總結(jié)整理而成,轉(zhuǎn)載請(qǐng)注明來(lái)自『移動(dòng)開(kāi)發(fā)前線』公眾號(hào)。

嘉賓介紹

鄒勇(網(wǎng)名小創(chuàng))蘑菇街支付金融資深安卓開(kāi)發(fā)工程師。自畢業(yè)以來(lái)一直從事Android開(kāi)發(fā)工作,先后工作于創(chuàng)新工廠、微策略以及蘑菇街。對(duì)單元測(cè)試和TDD情有獨(dú)鐘。

大家好,我是蘑菇街支付金融部門(mén)的鄒勇,花名叫小創(chuàng)。今天很高興跟大家分享一下安卓的單元測(cè)試在蘑菇街支付金融的實(shí)踐。下面,我們從為什么開(kāi)始。

為什么要寫(xiě)單元測(cè)試

首先要介紹為什么蘑菇街支付金融這邊會(huì)采用單元測(cè)試的實(shí)踐。說(shuō)起來(lái)比較巧,剛開(kāi)始的時(shí)候,只是我一個(gè)人會(huì)寫(xiě)單元測(cè)試。后來(lái)老板們知道了,覺(jué)得這是件很有價(jià)值的事情,于是就叫我負(fù)責(zé)我們組的單元測(cè)試這件事情。就這樣慢慢的,單元測(cè)試這件事情就成了我們這邊的正常實(shí)踐了。再后來(lái),在公司層面也開(kāi)始有一定的推廣。

要說(shuō)為什么要寫(xiě)單元測(cè)試的話,我相信大部分人都能承認(rèn)、也能理解單元測(cè)試在保證代碼質(zhì)量,防止bug或盡早發(fā)現(xiàn)bug這方面的作用,這可能是大家覺(jué)得單元測(cè)試最大的作用。然而我覺(jué)得,除了這方面的作用,單元測(cè)試還能在很大程度上改善代碼的設(shè)計(jì),同時(shí)還能節(jié)約時(shí)間,讓人工作起來(lái)更自信、更開(kāi)心,以及其他的一些好處。這些都是我的切身感受,我相信也是多數(shù)真正實(shí)踐過(guò)單元測(cè)試的人的切身感受,而不是為了宣傳這個(gè)東西而說(shuō)的好聽(tīng)的大話。

說(shuō)到節(jié)約時(shí)間,大家可能就會(huì)好奇了,寫(xiě)單元測(cè)試需要時(shí)間,維護(hù)單元測(cè)試代碼也需要時(shí)間,應(yīng)該更費(fèi)時(shí)間才對(duì)???

這就是在開(kāi)始分享之前,我想重點(diǎn)澄清的一點(diǎn),那就是,單元測(cè)試本身其實(shí)不會(huì)占用多少時(shí)間,相反,還會(huì)節(jié)約時(shí)間。只是:

  1. 學(xué)習(xí)如何做單元測(cè)試需要時(shí)間;

  2. 在一個(gè)沒(méi)有單元測(cè)試的項(xiàng)目中加入單元測(cè)試,需要一定的結(jié)構(gòu)調(diào)整的時(shí)間,因?yàn)橐粋€(gè)有單元測(cè)試跟沒(méi)有單元測(cè)試的項(xiàng)目,結(jié)構(gòu)上還是有較大不同的。

打個(gè)比方,開(kāi)車(chē)這件事情,需要很多時(shí)間嗎?我相信很少人會(huì)說(shuō)開(kāi)車(chē)這件事情需要很多時(shí)間,而是:

  1. 學(xué)習(xí)開(kāi)車(chē),需要一定的時(shí)間;

  2. 如果路面不平的話,那么修路需要一定的時(shí)間。單元測(cè)試也是類似的情況。

那為什么說(shuō)單元測(cè)試可以節(jié)約時(shí)間呢?簡(jiǎn)單說(shuō)幾點(diǎn):

  1. 如果沒(méi)有單元測(cè)試的話,就只能把a(bǔ)pp運(yùn)行起來(lái)測(cè)試,這比運(yùn)行一次單元測(cè)試要慢多了。

  2. 盡早發(fā)現(xiàn)bug,減少了debug和fixbug的時(shí)間。

  3. 重構(gòu)的時(shí)候,大大減少手動(dòng)驗(yàn)證重構(gòu)正確性的時(shí)間。

所以,我希望大家能去掉"沒(méi)時(shí)間寫(xiě)單元測(cè)試"這個(gè)印象,如果工作上安排太緊,沒(méi)有時(shí)間學(xué)習(xí)如何做單元測(cè)試的話,可以自己私底下學(xué),然后在慢慢應(yīng)用到項(xiàng)目中。

單元測(cè)試簡(jiǎn)單介紹

接下來(lái)介紹我們這邊是怎么做安卓單元測(cè)試的。首先澄清一下概念,在安卓上面寫(xiě)測(cè)試,有很多技術(shù)方案。有JUnit、Instrumentation test、Espresso、UiAutomator等等,還有第三方的Appium、Robotium、Calabash等等。我們現(xiàn)在講的是使用JUnit和其他的一些框架,寫(xiě)可以在我們開(kāi)發(fā)環(huán)境的JVM上面直接運(yùn)行的單元測(cè)試,其他的幾種其實(shí)都不屬于單元測(cè)試,而是集成測(cè)試或者叫Functional test等等。這兩者明顯的不同是,前者可以直接在開(kāi)發(fā)用的電腦,或者是CI上面的JVM上運(yùn)行,而且可以只運(yùn)行那么一小部分代碼,速度非常快。而后者必須要有模擬器或真機(jī),把整個(gè)project打包成一個(gè)app,然后上傳到模擬器或真機(jī)上,再運(yùn)行相關(guān)的代碼,速度相對(duì)來(lái)說(shuō)慢很多。

單元測(cè)試的定義相信大家都知道,就是為我們寫(xiě)的某一個(gè)代碼單元(比如一個(gè)方法)寫(xiě)的測(cè)試代碼。一個(gè)單元測(cè)試大概可以分為三個(gè)部分:

  1. setup:即new 出待測(cè)試的類,設(shè)置一些前提條件

  2. 執(zhí)行動(dòng)作:即調(diào)用被測(cè)類的被測(cè)方法,并獲取返回結(jié)果

  3. 驗(yàn)證結(jié)果:驗(yàn)證獲取的結(jié)果跟預(yù)期的結(jié)果是一樣的

然而一個(gè)類的方法分兩種,一種是有返回值的方法。一種是沒(méi)有返回值的方法,即void方法。對(duì)于有返回值的方法,固然測(cè)試起來(lái)是很容易的,但是對(duì)于沒(méi)有返回值的方法,該怎么測(cè)試呢?這里的關(guān)鍵是,怎么樣獲取這個(gè)方法的“返回結(jié)果”?

這里舉一個(gè)例子來(lái)說(shuō)明一下,順便澄清一個(gè)十分常見(jiàn)的誤解。比如說(shuō)有一個(gè)Activity,管他叫DataActivity,它有一個(gè)public void loadData()方法, 會(huì)去調(diào)用底層的DataModel類,異步的執(zhí)行一些網(wǎng)絡(luò)請(qǐng)求。當(dāng)網(wǎng)絡(luò)請(qǐng)求返回以后,更新用戶界面。

這里的loadData()方法是void的,它該怎么測(cè)試呢?一個(gè)最直接的反應(yīng)可能是,調(diào)用loadData()方法(當(dāng)然,實(shí)際可能是通過(guò)其他事件觸發(fā)),然后一段時(shí)間后,驗(yàn)證界面得到了更新。然而這種方法是錯(cuò)的,這種測(cè)試叫集成測(cè)試,而不是單元測(cè)試。因?yàn)樗婕暗胶芏鄠€(gè)方面,它涉及到DataModel、網(wǎng)絡(luò)服務(wù)器,以及網(wǎng)絡(luò)返回正確時(shí),DataActivity內(nèi)部的處理,等等。集成測(cè)試固然有它的必要性,但不是我們應(yīng)該最關(guān)注的地方,也不是最有價(jià)值的地方。我們應(yīng)該最關(guān)注的是單元測(cè)試。關(guān)于這一點(diǎn),有一個(gè)Test Pyramid的理論:

Test Pyramid理論基本大意是,單元測(cè)試是基礎(chǔ),是我們應(yīng)該花絕大多數(shù)時(shí)間去寫(xiě)的部分,而集成測(cè)試等應(yīng)該是冰山上面能看見(jiàn)的那一小部分。

那么對(duì)于這個(gè)case,正確的單元測(cè)試方法,應(yīng)該是去驗(yàn)證loadData()方法調(diào)用了DataModel的某個(gè)請(qǐng)求數(shù)據(jù)的方法,同時(shí)傳遞的參數(shù)是正確的。“調(diào)用了DataModel的方法,同時(shí)參數(shù)是。。?!?這個(gè)才是loadData()這個(gè)方法的“返回結(jié)果”。

Mock的概念以及Mockito框架

要驗(yàn)證某個(gè)對(duì)象的某個(gè)方法得到調(diào)用了,就涉及到mock的使用。這里對(duì)mock的概念做個(gè)簡(jiǎn)單介紹,以免很多同學(xué)不熟悉,mock就是創(chuàng)建一個(gè)虛假的、模擬的對(duì)象。在測(cè)試環(huán)境下,用來(lái)替換掉真實(shí)的對(duì)象。這樣就能達(dá)到兩個(gè)目的:

  1. 可以隨時(shí)指定mock對(duì)象的某個(gè)方法返回什么樣的值,或執(zhí)行什么樣的動(dòng)作。

  2. 可以驗(yàn)證mock對(duì)象的某個(gè)方法有沒(méi)有得到調(diào)用,或者是調(diào)用了多少次,參數(shù)是什么等等。

要使用mock,一般需要使用mock框架,目前安卓最常用的有兩個(gè),MockitoJMockit。兩者的區(qū)別是,前者不能mock static method和final class、final method,后者可以。我們依然采用的是Mockito,原因說(shuō)起來(lái)慚愧,是因?yàn)閯傞_(kāi)始并不知道JMockit這個(gè)東西,后來(lái)查了一些資料,看過(guò)很多對(duì)比Mockito和JMockit的文章,貌似大部分還是很看好JMockit的,只是有一個(gè)問(wèn)題,那就是跟robolectric的結(jié)合也有一些bug,同時(shí)使用姿勢(shì)跟Mockito有較大的不同,因此一直沒(méi)有抽時(shí)間去實(shí)踐過(guò)。這個(gè)希望以后能夠做進(jìn)一步的調(diào)查,到時(shí)候在給大家分享一下使用感受。

但是使用Mockito,就有一個(gè)問(wèn)題,那就是static method和final class、final method沒(méi)有辦法mock,對(duì)于這點(diǎn)如何解決,我們稍后會(huì)介紹到。

在測(cè)試環(huán)境中使用mock:依賴注入

接下來(lái)的一個(gè)問(wèn)題就是,如何在測(cè)試環(huán)境下,把DataModel換成mock的對(duì)象,而正式代碼中,DataModel又是正常的對(duì)象呢?

這個(gè)問(wèn)題也有兩種解決方案,一是使用專門(mén)的testing product flavor;二是使用依賴注入。第一種方案就是用一個(gè)專門(mén)的product flavor來(lái)做testing,在這個(gè)testing flavor里面,里面把需要mock的類寫(xiě)一份mock的implementation,然后通過(guò)factory提供給client,這個(gè)factory的接口在testing flavor和正式的flavor里面是一樣的,在跑testing的時(shí)候,專門(mén)使用這個(gè)testing flavor,這樣通過(guò)factory得到的就是mock的類。這種情況看起來(lái)很簡(jiǎn)單,但其實(shí)很不靈活,因?yàn)橹挥幸环Nmock實(shí)現(xiàn);此外,代碼會(huì)變得很丑陋,因?yàn)槟阈枰獮槊恳粋€(gè)dependency提供一個(gè)factory,會(huì)覺(jué)得很刻意;再者,多了一個(gè)flavor,很多gradle任務(wù)都會(huì)變得很慢。關(guān)于這種方案,可以參考這個(gè)視頻(https://www./watch?v=vdasFFfXKOY)。

因此,我們用的是第二種,依賴注入。先簡(jiǎn)單介紹一下依賴注入這個(gè)模式,他的基本理念是,某一個(gè)類(比如說(shuō)DataActivity),用到的內(nèi)部對(duì)象(比如說(shuō)DataModel)的創(chuàng)建過(guò)程不在DataActivity內(nèi)部去new,而是由外部去創(chuàng)建好DataModel的實(shí)例,然后通過(guò)某種方式set給DataActivity。這種模式應(yīng)用是非常廣泛的,尤其是在測(cè)試的時(shí)候。為了更方便的做依賴注入,如今有很多框架專門(mén)做這件事情,比如RoboGuiceDagger、Dagger2等等。我們用的是Dagger2,理由很簡(jiǎn)單,這是目前最好用的DI框架。

關(guān)于Dagger2的文章,之前我們?nèi)豪镆卜窒砹瞬簧?,但是好像我并沒(méi)有看到講述沒(méi)有關(guān)于如何在測(cè)試環(huán)境下使用Dagger2的文章,這個(gè)還是略感遺憾的。離開(kāi)單元測(cè)試,使用依賴注入就少了很有說(shuō)服力的一個(gè)理由。

那么這里我就介紹一下,怎么樣把Dagger2應(yīng)用到單元測(cè)試中。熟悉dagger2的童靴可能知道,Dagger2里面最關(guān)鍵的有兩個(gè)概念,ModuleComponent。Module是負(fù)責(zé)生成諸如DataModel這樣被別人(比如DataActivity)使用的類的地方。用術(shù)語(yǔ)的話,被別人使用的類DataModel叫Dependency,使用到了別的類的類DataActivity叫Client。而Component則是供Client使用Dependency的統(tǒng)一接口。也就是說(shuō),DataActivity通過(guò)Component,來(lái)得到一份DataModel的實(shí)例。

現(xiàn)在,關(guān)鍵的地方來(lái)了,Component本身是不生產(chǎn)dependency的,它只是搬運(yùn)工而已,真正生產(chǎn)dependency的地方在Module。所以,創(chuàng)建Component需要用到Module,不同的Module生產(chǎn)出不同的dependency。在正式代碼里面,我們使用正常的Module,生產(chǎn)正常的DataModel。而在測(cè)試環(huán)境中,我們寫(xiě)一個(gè)TestingModule,讓它繼承正常的Module,然后override掉生產(chǎn)DataModel的方法,讓它生產(chǎn)mock的DataModel。在跑單元測(cè)試的時(shí)候,使用這個(gè)TestingModule來(lái)創(chuàng)建Component,這樣的話,DataActivity通過(guò)Component得到的DataModel對(duì)象就是mock出來(lái)的DataModel對(duì)象。

使用這種方式,所有production code都不用專門(mén)為testing增加任何多余的代碼,同時(shí)還能得到依賴注入的其他好處。

Robolectric:解決Android單元測(cè)試最大的痛點(diǎn)

接下來(lái)講講Android單元測(cè)試最大的痛點(diǎn),那就是JVM上面運(yùn)行純JUnit單元測(cè)試時(shí)是不能使用Android相關(guān)的類的,因?yàn)槲覀冮_(kāi)發(fā)用到的安卓環(huán)境是沒(méi)有實(shí)現(xiàn)的,里面只定義了一些接口,所有方法的實(shí)現(xiàn)都是throw new RuntimeException("stub");,如果我們單元測(cè)試代碼里面用到了安卓相關(guān)的代碼的話,那么運(yùn)行時(shí)就會(huì)遇到RuntimeException("Stub")。

要解決這個(gè)問(wèn)題,一般來(lái)說(shuō)有三種方案:

  1. 使用Android提供的Instrumentation系統(tǒng),將單元測(cè)試代碼運(yùn)行在模擬器或者是真機(jī)上。

  2. 用一定的架構(gòu),比如MVP等等,將安卓相關(guān)的代碼隔離開(kāi)了,中間的Presenter或Model是存java實(shí)現(xiàn)的,可以在JVM上面測(cè)試。View或其他android相關(guān)的代碼則不測(cè)。

  3. 使用Robolectric框架,這個(gè)框架基本可以理解為在JVM上面實(shí)現(xiàn)了一套安卓的模擬環(huán)境,同時(shí)給安卓相關(guān)的類增加了其他一些增強(qiáng)的功能,以方便做單元測(cè)試,使用這個(gè)框架,我們就可以在JVM上面跑單元測(cè)試的時(shí)候,就可以使用安卓相關(guān)的類了。

第一種方案能work,但是速度非常慢,因?yàn)槊看芜\(yùn)行一次單元測(cè)試,都需要將整個(gè)項(xiàng)目打包成apk,上傳到模擬器或真機(jī)上,就跟運(yùn)行了一次app似得,這個(gè)顯然不是單元測(cè)試該有的速度,更無(wú)法做TDD。這種方案首先被否決。

剛開(kāi)始,我們采用的是Robolectric,原因有兩個(gè):

  1. 我們項(xiàng)目當(dāng)時(shí)還沒(méi)有比較清楚的架構(gòu),android跟純java代碼的隔離沒(méi)有做好;

  2. 很多安卓相關(guān)的代碼,還是需要測(cè)試的,比如說(shuō)自定義View等等。

然而慢慢的,我們的態(tài)度從擁抱Robolectric,到盡量不用它,盡量使用純java代碼去實(shí)現(xiàn)??赡艽蠹矣X(jué)得安卓相關(guān)的代碼會(huì)很多,而純java的很少,然而慢慢的你會(huì)發(fā)現(xiàn),其實(shí)不是這樣的,純java的代碼其實(shí)真不少,而且往往是核心的邏輯所在。之所以盡量不用Robolectric,是因?yàn)镽obolectric雖然相對(duì)于Instrumentation testing來(lái)說(shuō)快多了。但畢竟他也需要merge一些資源,build出來(lái)一個(gè)模擬的app,因此相對(duì)于純java和JUnit來(lái)說(shuō),這個(gè)速度依然是很慢的。

用具體的數(shù)字來(lái)對(duì)比說(shuō)明:

  • 運(yùn)行Instrumentation testing:幾十秒,取決于app的大小

  • Robolectric:10秒左右

  • JUnit:幾秒鐘之內(nèi)

當(dāng)然,雖然運(yùn)行一次Robolectric在10秒左右,但是對(duì)比運(yùn)行一次app,還是要快太多。因此,剛開(kāi)始的時(shí)候,從Robolectric開(kāi)始完全是OK的。

以上就是現(xiàn)在我們這邊單元測(cè)試用到的幾個(gè)基本技術(shù):JUnit4 + Mockito + Dagger2 + Robolectric?;緛?lái)說(shuō),并沒(méi)有什么黑科技,都是業(yè)界標(biāo)準(zhǔn)。

一個(gè)具體的案例

接下來(lái),我通過(guò)一個(gè)具體的案例,跟大家介紹一下,我們這邊的一個(gè)app,具體是怎么單測(cè)的。

這里是我們收銀臺(tái)界面的樣子:

假設(shè)Activity名字為CheckoutActivity,當(dāng)它啟動(dòng)的時(shí)候,CheckoutActivity會(huì)去調(diào)一個(gè)CheckoutModel的loadCheckoutData()方法,這個(gè)方法又會(huì)去調(diào)更底層的一個(gè)封裝了用戶認(rèn)證等信息的網(wǎng)絡(luò)請(qǐng)求Api類(mApi)的get方法,同時(shí)傳給這個(gè)Api類一個(gè)callback。這個(gè)callback的做的事情是將結(jié)果通過(guò)Otto Bus(mBus) post出去。CheckoutActivity里面Subscribe了這個(gè)Event(方法名是onCheckoutDataLoaded()),然后根據(jù)Event的值相應(yīng)的顯示數(shù)據(jù)或錯(cuò)誤信息。

代碼簡(jiǎn)寫(xiě)如下:

這里,CheckoutActivity里面的mCheckoutModel、CheckoutModel里面的mApi、CheckoutModel里面的mBus,都是通過(guò)Dagger2注入進(jìn)去的。在做單元測(cè)試的時(shí)候,這些都是mock。

對(duì)于這個(gè)流程,我們做了如下的單元測(cè)試:

  • CheckoutActivity啟動(dòng)單元測(cè)試:通過(guò)Robolectric提供的方法,啟動(dòng)一個(gè)Activity。驗(yàn)證里面的mCheckoutModel的loadCheckoutData()方法得到了調(diào)用,同時(shí)參數(shù)(訂單ID等)是對(duì)的。

  • CheckoutModel的loadCheckoutData單元測(cè)試1:調(diào)用CheckoutModel的loadCheckoutData()方法,驗(yàn)證里面的mApi對(duì)應(yīng)的get方法得到了調(diào)用,同時(shí)參數(shù)是對(duì)的。

  • CheckoutModel的loadCheckoutData單元測(cè)試2:mock Api類,指定當(dāng)它的get方法在收到某些調(diào)用的時(shí)候,直接調(diào)用傳入的callback的onSuccess方法,然后調(diào)用CheckoutModel的loadCheckoutData()方法,驗(yàn)證Otto bus的post方法得到了調(diào)用,并且參數(shù)是對(duì)的。

  • CheckoutModel的loadCheckoutData單元測(cè)試3:mock api類,指定當(dāng)它的get方法在收到某些調(diào)用的時(shí)候,直接調(diào)用傳入的callback的onFailure方法,然后調(diào)用CheckoutModel的loadCheckoutData()方法,驗(yàn)證Otto bus的post方法得到了調(diào)用,并且參數(shù)是對(duì)的。

  • CheckoutActivity的onCheckoutDataLoaded單元測(cè)試1:?jiǎn)?dòng)一個(gè)CheckoutActivity,調(diào)用他的onCheckoutDataLoaded(),傳入含有正確數(shù)據(jù)的Event,驗(yàn)證相應(yīng)的數(shù)據(jù)view顯示出來(lái)了

  • CheckoutActivity的onCheckoutDataLoaded單元測(cè)試2:?jiǎn)?dòng)一個(gè)CheckoutActivity,調(diào)用他的onCheckoutDataLoaded(),傳入含有錯(cuò)誤信息的Event,驗(yàn)證相應(yīng)的錯(cuò)誤提示view顯示出來(lái)了。

這里需要說(shuō)明的一點(diǎn)是,上面的每一個(gè)測(cè)試,都是獨(dú)立進(jìn)行的,不是說(shuō)下面的單元測(cè)試依賴于上面的?;蛘哒f(shuō)必須先做上面的,再做下面的。

這部分較為詳細(xì)的代碼放在github(https://github.com/ChrisZou/android-unit-testing-tutorial)上,groupshare這個(gè)package里面。

其他的問(wèn)題

以上就是我們這邊做單元測(cè)試用到的技術(shù),以及一個(gè)基本流程,下面聊聊其他的幾個(gè)問(wèn)題。

哪些東西需要測(cè)試呢?

  1. 所有的Model、Presenter/ViewModel、Api、Utils等類的public方法

  2. Data類除了getter、setter、toString、hashCode等一般自動(dòng)生成的方法之外的邏輯部分

  3. 自定義View的功能:比如set data以后,text有沒(méi)有顯示出來(lái)等等,簡(jiǎn)單的交互,比如click事件,負(fù)責(zé)的交互一般不測(cè),比如touch、滑動(dòng)事件等等。

  4. Activity的主要功能:比如view是不是存在、顯示數(shù)據(jù)、錯(cuò)誤信息、簡(jiǎn)單的點(diǎn)擊事件等。比較復(fù)雜的用戶交互比如onTouch,以及view的樣式、位置等等可以不測(cè)。因?yàn)椴缓脺y(cè)。

CI和code coverage: Jacoco

要把單元測(cè)試正式化,CI是非常重要的一步,我們有一個(gè)運(yùn)行Jenkins的CI server,每次開(kāi)發(fā)者push代碼到master branch的時(shí)候,會(huì)運(yùn)行一次單元測(cè)試的gradle task,同時(shí)使用Jacoco做code coverage。

這里有個(gè)坑要特別注意,那就是項(xiàng)目里面的gradle Jacoco插件和Jenkins的Jacoco插件的兼容性問(wèn)題。我們用的gradle Jacoco插件是7.1,更高版本的好像有問(wèn)題。然后對(duì)應(yīng)的Jenkins的Jacoco插件需要1.0.19或更低版本的,更高版本的jenkins plugin不支持低版本的gradle Jacoco項(xiàng)目版本。實(shí)際上,這點(diǎn)在Jenkins的Jacoco插件首頁(yè)就有說(shuō)明:

(點(diǎn)擊放大圖像)

但是我當(dāng)時(shí)沒(méi)注意,所以覆蓋率數(shù)據(jù)一直出不來(lái),折騰了好一會(huì),最后還是在同事的幫助下找到問(wèn)題了。

遇到的坑,以及好的practice建議

接下來(lái)講講我們遇到的一些坑,以及一些好的practice建議。

1. Native libary

無(wú)論是純JUnit還是Robolectric,都不支持load native library,會(huì)報(bào)UnsatisfiedLinkError的錯(cuò)。所以如果你的被測(cè)代碼里面用到了native lib,那么可能需要給System.loadLibrary加上try catch。

如果是被測(cè)代碼用到的第三方lib,而里面用到了native lib的話,一般有兩種解決辦法,一種是將用到native lib的第三方類外面自己在包一層,然后在測(cè)試的情況下mock掉。第二種是用Robolectric,給那個(gè)類創(chuàng)建一個(gè)shadow class。

第一種方法的好處是可以在測(cè)試的時(shí)候隨時(shí)改變這個(gè)類的返回值或行為,缺點(diǎn)是需要另外創(chuàng)建一個(gè)wrapper類,會(huì)有點(diǎn)繁瑣。第二種方式不能隨時(shí)改變這個(gè)類的行為,但是寫(xiě)起來(lái)非常簡(jiǎn)單。所以,看自己的需要,選擇相應(yīng)的方法。

這兩種方法,也是解決static method, final class/method不能mock的主要方式。

2. 盡量寫(xiě)出易于測(cè)試的代碼

static method、直接new object、singleton、Global state等等這些都是一些不利于測(cè)試的代碼方式,應(yīng)該盡量避免,用依賴注入來(lái)代替這些方式。

3. 不要重復(fù)你的unit test

比如說(shuō)你使用了一個(gè)builder模式來(lái)創(chuàng)建了一個(gè)類,這個(gè)builder有一個(gè)validator,來(lái)validate一些參數(shù)情況。那么這種情況,builder跟validator分開(kāi)測(cè),用各種正確的錯(cuò)誤的參數(shù)情況去測(cè)試validator,然后測(cè)builder的時(shí)候,就不用遍歷各種有效的跟無(wú)效的參數(shù)去測(cè)試了。

因?yàn)槿绻@樣的話,到時(shí)候Validator的邏輯改了,那么針對(duì)Validator的測(cè)試跟針對(duì)Builder的測(cè)試都要修改,這個(gè)其實(shí)是重復(fù)的。這里只需要測(cè)試這個(gè)builder里面有一個(gè)Validator就好了。

4. 公共的單元測(cè)試library

如果你們公司也是組件化開(kāi)發(fā)的話,抽出一個(gè)公共的單元測(cè)試類庫(kù)來(lái)做單元測(cè)試,里面可以放一些公共的helper、utils、rules等等,這個(gè)可以極大的提高寫(xiě)單元測(cè)試的速度。

5. 把安卓里面的“純java”代碼copy一份到自己的項(xiàng)目里面

安卓里面有些類其實(shí)跟安卓沒(méi)太大關(guān)系的,比如說(shuō)TextUtils、Color等等,這些類完全可以把代碼copy出來(lái),放到自己的項(xiàng)目里面,然后其他地方就用這個(gè)類,這樣也能部分?jǐn)[脫android的依賴,使用JUnit而不是Robolectric,提高運(yùn)行test的速度。

6. 充分發(fā)揮JUnit Rule的作用

JUnit Rule是個(gè)很強(qiáng)大的工具,然而知道的人卻不多。它的基本作用是,讓你在執(zhí)行某個(gè)測(cè)試方法前后,可以做一些事情。如果你的好幾個(gè)測(cè)試類里面有很多的共同的setup、teardown工作,你可能會(huì)傾向于使用繼承,結(jié)合@Before、@After來(lái)減少duplication,這里更建議大家使用JUnit Rule來(lái)實(shí)現(xiàn)這個(gè)目的,而不是用繼承,這樣可以有更大的靈活性。

比如,為了方便測(cè)試Activity的method,我們有一個(gè)ActivityRule,在跑一個(gè)測(cè)試方法之會(huì)啟動(dòng)target Activity,然后跑完以后自動(dòng)finish這個(gè)activity。

其中一個(gè)比較有趣的用JUnit Rule實(shí)現(xiàn)的功能,是實(shí)現(xiàn)類似于BDD測(cè)試框架的命名方式。做單元測(cè)試的時(shí)候,你經(jīng)常需要為同一個(gè)方法寫(xiě)好幾個(gè)測(cè)試方法,每個(gè)測(cè)試方法測(cè)試不同的點(diǎn)。為了讓命名更具可讀性,我們往往會(huì)把名字寫(xiě)的很長(zhǎng),在這種情況下,如果用駝峰命名的話,需要不斷切換大小寫(xiě),寫(xiě)起來(lái)麻煩,可讀性也不高。如果用下劃線的話,寫(xiě)起來(lái)也很麻煩。如果你使用過(guò)BDD的一些框架(比如RSpec、CucumberJasmine等),你就會(huì)異常懷念那種“命名”方式。如果你沒(méi)用過(guò)的話,那種“命名”方式大概是這樣的:

這里的關(guān)鍵是,當(dāng)測(cè)試方法失敗的時(shí)候,這個(gè)字符串是要能被加到錯(cuò)誤信息里面的。我們做了個(gè)JUnit Rule來(lái)達(dá)到這個(gè)效果。做法是結(jié)合一個(gè)自定義的annotation,這個(gè)annotation接收一個(gè)String,來(lái)描述這個(gè)測(cè)試方法的測(cè)試目的。在Rule里面將這個(gè)annotation讀出來(lái),如果測(cè)試沒(méi)通過(guò)的話,把這個(gè)描述性的String加到輸出的error message里面。這樣在批量運(yùn)行的時(shí)候,一看就知道沒(méi)通過(guò)的測(cè)試是測(cè)什么東西的。而測(cè)試方法的命名則可以比較隨意。
達(dá)到的效果如下:

如果運(yùn)行失敗,得到如下的結(jié)果

關(guān)于JUnit Rule的使用,大家可以自行g(shù)oogle一下,也不難。

7. 善于利用AndroidStudio來(lái)加快你寫(xiě)測(cè)試的速度

AndroidStudio有很多feature可以幫助我們更快的寫(xiě)代碼,比如code generation和live template。這點(diǎn)對(duì)于寫(xiě)正式代碼也適用,不過(guò)對(duì)于寫(xiě)測(cè)試代碼來(lái)說(shuō),效果更為突出。因?yàn)榇蟛糠譁y(cè)試代碼的結(jié)構(gòu)、風(fēng)格都是類似的,在這里live template能起非常大的作用。此外,如果你先寫(xiě)測(cè)試,可以直接寫(xiě)一些還不存在的Class或method,然后alt+enter讓AndroidStudio自動(dòng)幫你生成。

8. 不要追求完美

剛開(kāi)始的時(shí)候,不用追求測(cè)試代碼的質(zhì)量,也不用追求完美,如果有些地方不好寫(xiě)測(cè)試,可以先放放,以后再來(lái)補(bǔ),有部分測(cè)試總比沒(méi)有測(cè)試好。Martin Fowler說(shuō)過(guò)

Imperfect tests, run frequently, are much better than perfect tests that are never written at all.

然而等你熟悉寫(xiě)測(cè)試的方法以后,強(qiáng)烈建議先寫(xiě)測(cè)試!因?yàn)槿绻阆葘?xiě)了正式代碼,那你對(duì)這寫(xiě)代碼是如何work的已經(jīng)有一個(gè)印象了,因此你往往會(huì)寫(xiě)出能順利通過(guò)的測(cè)試,而忽略一些會(huì)讓測(cè)試不通過(guò)的情況。如果先寫(xiě)測(cè)試,則能考慮得更全面。

9. 未來(lái)的打算

使用Groovy和RoboSpock或者是Kotlin和Spek,真正實(shí)現(xiàn)BDD,這是很可能的事情,只是目前我們這邊還沒(méi)太多那方面的實(shí)踐,因此就不說(shuō)太多了。以后有一定實(shí)踐了,到時(shí)候可以再更大家交流。

文中部分代碼:https://github.com/ChrisZou/android-unit-testing-tutorial

QA環(huán)節(jié)

Q:如何測(cè)試界面交互?如點(diǎn)擊拖動(dòng)等。

A:Robolectric提供了非常豐富的測(cè)試交互的方式,比如findViewById(id).performClick()。基本上,使用Robolectric,你可以像正常寫(xiě)安卓代碼那樣寫(xiě)測(cè)試代碼。甚至正常情況下沒(méi)有的方法,Robolectric也提供了。

Q:我也是后來(lái)才接觸代碼測(cè)試的,然后開(kāi)始喜歡上寫(xiě)代碼測(cè)試,但當(dāng)嘗試為以前的代碼寫(xiě)代碼測(cè)試的時(shí)候,發(fā)現(xiàn)以前的結(jié)構(gòu)很難寫(xiě)代碼測(cè)試,請(qǐng)問(wèn)你們也有遇到這種情況么?如何解決。

A:這的確是比較頭疼的問(wèn)題,建議可以看看《Working Effective With Legacy Code》一般來(lái)說(shuō)就是挑選一個(gè)比較好下手的地方,做好隔離寫(xiě)好測(cè)試,在重構(gòu)。那本書(shū)里面提出了很多簡(jiǎn)單除暴的方式,比如把一個(gè)方法或變量從private改成public等等。

Q:自繪控件一般怎么去做自動(dòng)化測(cè)試?

A: 自定義控件一般只測(cè)他的功能性的部分,樣式、動(dòng)畫(huà)這樣的一般不測(cè)。

測(cè)試方式基本就是把這個(gè)控件new 出來(lái),然后調(diào)用它的public 方法,驗(yàn)證它的text是不是正確等等。或者是相應(yīng)的事件有沒(méi)有觸發(fā),這個(gè)借助Robolectric可以做到。

Q:業(yè)務(wù)測(cè)試數(shù)據(jù),是自己本地寫(xiě)的邏輯,還是結(jié)合服務(wù)器的真實(shí)邏輯?

A:對(duì)于單元測(cè)試來(lái)說(shuō),一般是自己mock服務(wù)器的返回結(jié)果,因?yàn)榉?wù)器返回結(jié)果是不是正確的,其實(shí)不是我們應(yīng)該測(cè)的情況,而是服務(wù)器應(yīng)該測(cè)的情況,我們要測(cè)的,是服務(wù)器返回正確的結(jié)果我們就顯示正確的結(jié)果,服務(wù)器返回錯(cuò)誤的結(jié)果,我們就顯示錯(cuò)誤的返回信息。

Q:MVP的情況下view和presenter的回調(diào)函數(shù)需要做測(cè)試嗎,如果需要怎么做?

A:要測(cè),把presenter new出來(lái),直接調(diào)用它的那個(gè)方法就是了。

Q:對(duì)于依賴環(huán)境的測(cè)試,比如有無(wú)網(wǎng)絡(luò),不同的網(wǎng)絡(luò)測(cè)試類型,不同的網(wǎng)絡(luò)類型,網(wǎng)絡(luò)超時(shí)等,這種怎么去做單元測(cè)試比較好?在比如測(cè)試試寫(xiě)文件的方法,怎么去構(gòu)造剩余空間不足、空間足夠的環(huán)境?

A:這些情況需要借助系統(tǒng)的api(比如NetworkManager),去判斷情況,這種情況可以把這些系統(tǒng)的api mock掉,指定讓他返回你想要指定的結(jié)果。

Q:為啥方法名不是駝峰命名法?

A:因?yàn)槌3P枰獮橥粋€(gè)方法寫(xiě)好幾個(gè)測(cè)試方法,每個(gè)方法測(cè)試的目的可能不一樣,這個(gè)時(shí)候往往會(huì)把測(cè)試方法寫(xiě)的很長(zhǎng),需要一段的駝峰大小寫(xiě)切換,這樣可讀性不高,寫(xiě)起來(lái)又麻煩,或者是用下劃線去分隔,這樣寫(xiě)起來(lái)又很麻煩。因此我們特別寫(xiě)了那個(gè)annotation和junit rule,目的就是可以讓單元測(cè)試的方法命名可以隨意一點(diǎn)。

Q:robolectric一般只能模擬點(diǎn)擊到一個(gè)子控件,但是自繪的控件可能不滿足,自繪控件一般是為了較少layout的嵌套,而實(shí)現(xiàn)自繪的,點(diǎn)擊控件的不同區(qū)域可能會(huì)觸發(fā)不同的事件,以前我們的做法非常拿到,需要專門(mén)去根據(jù)這個(gè)自繪控件去這一大堆的測(cè)試代碼,不知道有沒(méi)有什么好的方法?

A:這種情況的確沒(méi)碰到過(guò),我覺(jué)得可以看看Robolectric有沒(méi)有指定點(diǎn)擊這個(gè)view的某個(gè)坐標(biāo)的方法,以我的經(jīng)驗(yàn),要實(shí)現(xiàn)你說(shuō)的那種功能,應(yīng)該是在這個(gè)view的ontouch時(shí)間里面去處理把,可以看看Robolectric有沒(méi)有類似的模擬ontouch的方法,估計(jì)應(yīng)該是有的。


感謝徐川對(duì)本文的審校。

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購(gòu)買(mǎi)等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多