長(zhǎng)久以來(lái)椅挣,測(cè)試對(duì)于很多安卓開(kāi)發(fā)小白們都是一個(gè)盲區(qū)。這個(gè)很大程度上是因?yàn)樽鯽pp祝辣,大家都習(xí)慣了自己手動(dòng)測(cè)試feature贴妻,畢竟是所見(jiàn)即所得的東西切油,點(diǎn)幾個(gè)按鈕看看能不能按照要求展示幾個(gè)頁(yè)面好像并不是那么難蝙斜。其次是因?yàn)楹芏啻a寫(xiě)的并不是特別可測(cè) (比如代碼都寫(xiě)在activity里面),導(dǎo)致沒(méi)法進(jìn)行單元測(cè)試澎胡。
以上的幾個(gè)原因孕荠,最終導(dǎo)致了很多接觸安卓開(kāi)發(fā)沒(méi)多久的朋友(尤其是在小廠娩鹉,對(duì)迭代速度要求更快的地方)沒(méi)怎么接觸過(guò)安卓的單元測(cè)試,也不知道test coverage是什么稚伍,更加意識(shí)不到單元測(cè)試的重要性弯予。產(chǎn)生了一種類似于咱們大學(xué)剛接觸高等數(shù)學(xué)證明題的感覺(jué):
對(duì)應(yīng)到咱們今天講的測(cè)試,很多人在看完同事寫(xiě)的測(cè)試代碼之后也有類似的震驚个曙。"這還要測(cè)锈嫩?" “這也能測(cè)?”
今天我想著重講一下安卓開(kāi)發(fā)中單元測(cè)試的意義垦搬,來(lái)說(shuō)明“這也要測(cè)”的意義呼寸。同時(shí)提供一些安卓測(cè)試中的小技巧,把“這也能測(cè)”的問(wèn)題一并給解決 :)
單元測(cè)試的意義
對(duì)于安卓開(kāi)發(fā)來(lái)說(shuō)猴贰,大部分小白們對(duì)于單元測(cè)試處于懵逼的狀態(tài)对雪。不知道測(cè)試有啥用。我剛剛開(kāi)始工作的時(shí)候就特別不喜歡寫(xiě)test米绕,覺(jué)得是浪費(fèi)時(shí)間瑟捣。我的想法是,就算單元測(cè)試成功了栅干,你的app跑起來(lái)也不一定能work啊迈套。。碱鳞。 所以還不如專心在手動(dòng)測(cè)試上交汤。
其實(shí)這個(gè)想法也不能說(shuō)完全錯(cuò),甚至可以說(shuō)是對(duì)了一半劫笙。因?yàn)閱卧獪y(cè)試成功不一定代碼app的功能就沒(méi)問(wèn)題芙扎。但是反過(guò)來(lái)說(shuō),如果單元測(cè)試都不對(duì)填大,那app的功能肯定有問(wèn)題戒洼。
軟件開(kāi)發(fā)中有一個(gè)大假設(shè),就是如果你的每個(gè)模塊都能自己獨(dú)立且正確運(yùn)行的話允华,這個(gè)軟件就大概率能正確的運(yùn)行圈浇。比如,如果我們app中每個(gè)class都能通過(guò)各種的獨(dú)立單元測(cè)試靴寂,那么把他們拼接起來(lái)這個(gè)app應(yīng)該就沒(méi)毛病了磷蜀。
單元測(cè)試位于軟件開(kāi)發(fā)測(cè)試的金字塔的最底層,也是最重要的那一層百炬。單元測(cè)試都跑不過(guò)褐隆,就別談集成測(cè)試 , UI 測(cè)試了剖踊。安卓也不例外庶弃。
可能光這么說(shuō)大家還是體會(huì)不到單元測(cè)試的好處衫贬。那么我就選一個(gè)方面來(lái)具體的說(shuō)說(shuō)單元測(cè)試在實(shí)際開(kāi)發(fā)過(guò)程中,可以給我們帶來(lái)什么好處歇攻。
怎么改固惯?單元測(cè)試說(shuō)了算
在大廠工作的朋友肯定都有過(guò)接手別人項(xiàng)目的經(jīng)驗(yàn),當(dāng)你在嘗試修改某一個(gè)class的時(shí)候缴守,你怎么確定你添加的代碼就是對(duì)的呢 (在不運(yùn)行app做手動(dòng)測(cè)試之前)葬毫?
答案就是單元測(cè)試。unit test在很多情況下屡穗,可以當(dāng)做你修改代碼的規(guī)則. class A 哪里改了會(huì)影響到class B供常,都可以在跑unit test之后發(fā)現(xiàn),這也是你作為一個(gè)項(xiàng)目后來(lái)者了解細(xì)節(jié)的方式鸡捐。
用一個(gè)我以前自己類似的經(jīng)歷做例子栈暇。假如有以下MVP pattern的代碼:
class Presenter{
enum Status{
LARGE,
MEDIUM
}
public Status getFinancialStatus(int size){
if(size < 1000){
return Status.SMALL
}
else{
return Status.MEDIUM
}
}
}
以上代碼通過(guò)房子size大小判斷是small還是medium。現(xiàn)在產(chǎn)品經(jīng)理說(shuō)咱給他添加一個(gè)large的size把箍镜。于是你興高采烈改了代碼源祈,簡(jiǎn)單的很,不就是加一個(gè)if else么:
class Presenter{
enum Status{
LARGE,
SMALL,
MEDIUM
}
public Status getFinancialStatus(int size){
if(size < 1000){
return Status.SMALL
}
else if( size < 6000){
return Status.MEDIUM
}
else{
return Status.LARGE
}
}
}
結(jié)果app跑起來(lái)之后crash了色迂!
仔細(xì)一看香缺,原來(lái)Activity里面有這樣的代碼(這里的例子都只是模擬場(chǎng)景,為的是說(shuō)明測(cè)試的重要性歇僧,現(xiàn)實(shí)開(kāi)發(fā)中肯定不可能把這種條件判斷寫(xiě)在activity里面)
class HouseActivity extends Activity{
public void display(int size){
if(size > 10000){
throws IllegalStatusException()
}
Status status = presenter.getFinancialStatus(size)
.....其他邏輯
}
}
HouseActivity 的單元測(cè)試長(zhǎng)這個(gè)樣子:
@Test(expected = IndexOutOfBoundsException.class)
public void sizeTooLargeAssertException(){
activity.display(30000)
}
原來(lái)我們?cè)赼ctivity里面有邏輯图张,限制最多只能展示大于10000的size,如果我在運(yùn)行app之前就已經(jīng)實(shí)現(xiàn)跑過(guò)了HouseActivity 的單元測(cè)試,我就會(huì)提前知道原來(lái)我們的app不處理大于10000的數(shù)據(jù)诈悍。
以上只是一個(gè)簡(jiǎn)單的例子祸轮,但是這個(gè)例子說(shuō)明了一個(gè)很大的問(wèn)題,就是在提交你的代碼之前侥钳,運(yùn)行一個(gè)有效的單元測(cè)試是有多么重要适袜。他可以幫你測(cè)試修改的代碼會(huì)對(duì)其他模塊有什么影響,如果破壞了既有的測(cè)試(規(guī)則)舷夺,你應(yīng)該怎么處理苦酱。要知道很多代碼在修改之后,你以為你打開(kāi)app手動(dòng)測(cè)試一下通過(guò)了肯定就沒(méi)問(wèn)題给猾,但是你有沒(méi)有想過(guò)疫萤,這個(gè)代碼,這個(gè)類敢伸,會(huì)不會(huì)對(duì)其他頁(yè)面有影響扯饶。這個(gè)就是單元測(cè)試的作用:
制定一套既有的規(guī)則,所有新增/修改的代碼要按照這個(gè)規(guī)則來(lái)運(yùn)行。
測(cè)試這種規(guī)則帝际,要比你手動(dòng)打開(kāi)app測(cè)試更加健壯且快速(compile 一個(gè)完整app vs 運(yùn)行 一個(gè)純java的測(cè)試)。
在理想狀態(tài)下饶辙,每一個(gè)類的每一行代碼都要被unit test cover蹲诀,一套單元測(cè)試的coverage(覆蓋率)可以體現(xiàn)你給你代碼制定規(guī)則的數(shù)量和健壯程度。比如說(shuō)還是用上面的例子:
class Presenter{
enum Status{
LARGE,
MEDIUM
}
public Status getFinancialStatus(int size){
if(size < xxx){
return Status.SMALL
}
else{
return Status.MEDIUM
}
}
}
你的測(cè)試如果只有:
@Test
public void smallSizeReturnSmallStatus(){
int size = 90
assertThat(presenter.getFinancialStatus(size)).isEqualTo(Status.SMALL)
}
那你對(duì)Presenter這個(gè)類的coverage只有50%弃揽。為什么脯爪?因?yàn)槟愕膖est沒(méi)有覆蓋到else這個(gè)語(yǔ)句,補(bǔ)上一下測(cè)試:
@Test
public void largeSizeReturnLargeStatus(){
int size = 3000
assertThat(presenter.getFinancialStatus(size)).isEqualTo(Status.MEDIUM)
}
跑完這兩個(gè)測(cè)試矿微,你的presener的單元測(cè)試覆蓋率就是100%了痕慢,恭喜!
順便說(shuō)一句涌矢,現(xiàn)在android studio已經(jīng)支持顯示unit test 覆蓋率了,有興趣可以看看
https://developer.android.com/studio/test
比如我有個(gè)dummy class
給它的else語(yǔ)句加個(gè)test:
Android Studio不僅會(huì)給出覆蓋率等重要數(shù)據(jù)疙驾,還會(huì)給代碼加上標(biāo)記悍募,這樣開(kāi)發(fā)者就可以輕易的看出來(lái)哪一行代碼沒(méi)有被測(cè)試覆蓋,是否需要加測(cè)試(原諒色代表被覆蓋,紅色代表沒(méi)有被覆蓋)
測(cè)什么金拒?
我們都知道一個(gè)類的單元測(cè)試是要保證這個(gè)類能正常運(yùn)行。那么什么是類能正常運(yùn)行呢兰珍?這個(gè)標(biāo)準(zhǔn)是什么温亲?
還是以例子為主:
class HousePagePresenter{
//http service client
private HouseApiService service = new HouseApiService()
public Data getHouseData(int size){
if(size < 1000){
return service.call(Status.SMALL)
}
else{
return service.call(Status.MEDIUM)
}
}
}
HouseApiService 是一個(gè)做http call的類,參數(shù)是Status匕得。當(dāng)size小于1000就傳SMALL继榆,反之MEDIUM。那對(duì)于HousePagePresenter來(lái)說(shuō)汁掠,這個(gè)類怎么樣運(yùn)行才是正確的略吨?
那就是當(dāng)getHouseData() 傳入的參數(shù)小于1000的時(shí)候,service 類成員要調(diào)用call 方法考阱,而且參數(shù)是SMALL晋南,反之是MEDIUM。
HousePagePresenter只需要保證在合適的size的前提下羔砾,service能調(diào)用call并且使用正確的Status就行了负间。我們只在乎service有沒(méi)有做出正確的動(dòng)作,至于動(dòng)作結(jié)果姜凄,不重要政溃!
怎么測(cè)?
那說(shuō)回來(lái)态秧,這個(gè)怎么測(cè)董虱?
首先,要給一個(gè)代碼做測(cè)試,要先保證他是可測(cè)的愤诱。上面的代碼其實(shí)是沒(méi)法測(cè)試的云头!Not testable.因?yàn)镠ouseApiService作為私有對(duì)象,我們沒(méi)辦法模擬(Mock)它淫半,從而無(wú)法驗(yàn)證它的行為在一定條件下是否符合我們的期望溃槐。
正確的做法是,要做“依賴注入”科吭。把HousePagePresenter對(duì)因?yàn)镠ouseApiService的依賴昏滴,從類對(duì)象的方式轉(zhuǎn)移成別的方式,或者說(shuō)可測(cè)的方式对人,比如移到構(gòu)造函數(shù)里面(也可以通過(guò)別的方式比如說(shuō)setter)谣殊。
class HousePagePresenter{
public HousePagePresenter(HouseApiService service){
this.service = service
}
//http service client
private HouseApiService service;
public Data getHouseData(int size){
if(size < 1000){
return service.call(Status.SMALL)
}
else{
return service.call(Status.MEDIUM)
}
}
}
這樣的好處可以說(shuō)是非常大。這樣牺弄,我們?cè)跍y(cè)試HousePagePresenter類的時(shí)候姻几,就不需要真正的創(chuàng)建一個(gè)HouseApiService了,而是可以模擬:
@Test
public void smallSizeServiceCall(){
int size = 900
HouseApiService service = mock(service.class)
HousePagePresenter presenter = new HousePagePresenter(service)
presenter.getHouseData(size)
//驗(yàn)證service是不是真正調(diào)用了call势告,并且參數(shù)也是期望值
verify(service).call(Status.SMALL)
}
通過(guò)把Service移到構(gòu)造函數(shù)鲜棠,讓代碼可以通過(guò)mockito mock的方式生成一個(gè)模擬的Service,這個(gè)service不會(huì)做任何真正的http call培慌,只會(huì)記錄自己call()方法被調(diào)用的情況豁陆。這就夠了,這已經(jīng)能證明HousePagePresenter這個(gè)類沒(méi)問(wèn)題吵护,如果service有問(wèn)題盒音,那應(yīng)該在service自己的單元測(cè)試?yán)锩娼鉀Q。
具體怎么解決依賴注入馅而,可以稍微看一下一個(gè)視頻
https://www.bilibili.com/video/BV1e54y1S72A/?spm_id_from=333.788.recommend_more_video.-1
有人覺(jué)得只有用dagger這類依賴注入庫(kù)才叫依賴注入祥诽,這是一個(gè)常見(jiàn)的誤解。想了解更多的朋友可以自行搜索一下瓮恭。
安卓控件沒(méi)法測(cè)雄坪?
很多朋友會(huì)說(shuō)自己有很多邏輯需要安卓本身的控件支持,這部分真的沒(méi)法測(cè)啊屯蹦。乖乖维哈,谷歌已經(jīng)給我們提供了從UI到系統(tǒng)api的全家桶,想偷懶不寫(xiě)test登澜?不存在的阔挠。。脑蠕。
純UI的單元測(cè)試
對(duì)于fragment 和 activity本身的UI測(cè)試购撼,Roboletric 框架提供了ActivityRule支持跪削,允許開(kāi)發(fā)者在unit test中啟動(dòng)測(cè)試activity,從而啟動(dòng)fragment迂求。同時(shí)配合Espresso框架可以再unit test代碼中獲取View對(duì)象碾盐,達(dá)到測(cè)試View的目的。
比如::
//設(shè)置測(cè)試activity類
private activityScenarioRule = ActivityScenarioRule(TestActivity.class)
@Before
void setup{
//啟動(dòng)測(cè)試fragment
activityScenarioRule.scenario.onActivity{
activity.setFragmemnt(new TestFragment());
}
}
@Test
void whenButtonClicked_executeMethod(){
// 通過(guò)onView獲取button揩局,手動(dòng)模擬點(diǎn)擊事件
onView(R.id.button).performClick();
verify(presenter).getHouseData()
}
結(jié)合ActivityScenarioRule和Espresso毫玖,我們可以把Fragment或者Activity當(dāng)成一個(gè)正常的再正常不過(guò)的類來(lái)進(jìn)行測(cè)試了。我剛剛?cè)肼毠雀璧臅r(shí)候就想偷懶不給UI寫(xiě)test谐腰,找借口說(shuō)UI測(cè)不了孕豹,直接被senior大哥焦作人涩盾。十气。。
系統(tǒng)API
假如你的方法里需要獲得當(dāng)前手機(jī)運(yùn)營(yíng)商信息春霍,那你可能需要TelephonyManager
這個(gè)系統(tǒng)api來(lái)幫忙砸西。
fun getCarrierId(){
val manager: TelephonyManager = applicationContext.getSystemService(TelephonyManager::class.java)
if(manager.simCarrierId == 1){
//做什么邏輯
}
else{
//做其他邏輯
}
}
這種情況,你需要Shadow object來(lái)幫忙啦址儒!
Roboletric 提供各種系統(tǒng)級(jí)別API的shadow芹枷,幫助你在測(cè)試的時(shí)候模擬不同的其情況。
比如:
@Test
fun testCarrierId(){
val shadowTelephonyManager = Shadows.of(context.getSystemService(TelephonyManager::class::java))
//給shadow強(qiáng)行設(shè)置一個(gè)值
shadowTelephonyManager.setSimCarrierId(-1);
//繼續(xù)測(cè)試getCarrierId()方法
}
通過(guò)shadow莲趣,我們就可以測(cè)試那些含有系統(tǒng)級(jí)別api的類和方法了鸳慈。
有了Roboletric之后,以前那些復(fù)雜的UI喧伞,和系統(tǒng)api測(cè)試再也不是問(wèn)題了走芋。我寫(xiě)了這么久測(cè)試之后發(fā)現(xiàn),基本上沒(méi)有不可以shadow潘鲫,或者不能mock的東西了翁逞。每當(dāng)我發(fā)現(xiàn)自己的代碼的某一行測(cè)不了,那肯定是我的代碼沒(méi)有寫(xiě)成可測(cè)的形式溉仑。
結(jié)尾
來(lái)谷歌的這幾個(gè)月可以說(shuō)我在各種被教做人挖函,知識(shí)非常匱乏。谷歌在測(cè)試浊竟,和代碼規(guī)范方面比之前亞麻可以說(shuō)嚴(yán)了不只一個(gè)級(jí)別怨喘,第一個(gè)月我就打破了自己修改代碼的記錄,一個(gè)PR修改了30次振定。哲思。。
但是被教做人的同時(shí)我也學(xué)到了不少吩案,尤其是unit test棚赔。以前在亞麻和創(chuàng)業(yè)公司隨意慣了,寫(xiě)測(cè)試?不存在的靠益。丧肴。。在寫(xiě)崩組內(nèi)系統(tǒng)次數(shù)逐漸增加之后胧后,我也漸漸意識(shí)到了單元測(cè)試的重要性芋浮,也想著趁腦子還有貨和大家多分享一下,也請(qǐng)各位大牛多多指正壳快!
祝大家五一快樂(lè)纸巷!羨慕國(guó)內(nèi)的朋友已經(jīng)到處游山玩水了。眶痰。瘤旨。美帝疫情還是一天新增好幾萬(wàn)。竖伯。存哲。。