給安卓開發小白們的unit test指南 - 這也能測?這也要測?

長久以來,測試對於很多安卓開發小白們都是一個盲區。這個很大程度上是因爲做app,大家都習慣了自己手動測試feature,畢竟是所見即所得的東西,點幾個按鈕看看能不能按照要求展示幾個頁面好像並不是那麼難。其次是因爲很多代碼寫的並不是特別可測 (比如代碼都寫在activity裏面),導致沒法進行單元測試。

以上的幾個原因,最終導致了很多接觸安卓開發沒多久的朋友(尤其是在小廠,對迭代速度要求更快的地方)沒怎麼接觸過安卓的單元測試,也不知道test coverage是什麼,更加意識不到單元測試的重要性。產生了一種類似於咱們大學剛接觸高等數學證明題的感覺:

對應到咱們今天講的測試,很多人在看完同事寫的測試代碼之後也有類似的震驚。"這還要測?" “這也能測?”

今天我想着重講一下安卓開發中單元測試的意義,來說明“這也要測”的意義。同時提供一些安卓測試中的小技巧,把“這也能測”的問題一併給解決 :)

單元測試的意義

對於安卓開發來說,大部分小白們對於單元測試處於懵逼的狀態。不知道測試有啥用。我剛剛開始工作的時候就特別不喜歡寫test,覺得是浪費時間。我的想法是,就算單元測試成功了,你的app跑起來也不一定能work啊。。。 所以還不如專心在手動測試上。

其實這個想法也不能說完全錯,甚至可以說是對了一半。因爲單元測試成功不一定代碼app的功能就沒問題。但是反過來說,如果單元測試都不對,那app的功能肯定有問題。

軟件開發中有一個大假設,就是如果你的每個模塊都能自己獨立且正確運行的話,這個軟件就大概率能正確的運行。比如,如果我們app中每個class都能通過各種的獨立單元測試,那麼把他們拼接起來這個app應該就沒毛病了。

單元測試位於軟件開發測試的金字塔的最底層,也是最重要的那一層。單元測試都跑不過,就別談集成測試 , UI 測試了。安卓也不例外。

可能光這麼說大家還是體會不到單元測試的好處。那麼我就選一個方面來具體的說說單元測試在實際開發過程中,可以給我們帶來什麼好處。

怎麼改?單元測試說了算

在大廠工作的朋友肯定都有過接手別人項目的經驗,當你在嘗試修改某一個class的時候,你怎麼確定你添加的代碼就是對的呢 (在不運行app做手動測試之前)?

答案就是單元測試。unit test在很多情況下,可以當做你修改代碼的規則. class A 哪裏改了會影響到class B,都可以在跑unit test之後發現,這也是你作爲一個項目後來者瞭解細節的方式。

用一個我以前自己類似的經歷做例子。假如有以下MVP pattern的代碼:

class Presenter{
    enum Status{
       LARGE,
       MEDIUM
    }
    
    public Status getFinancialStatus(int size){
        if(size < 1000){
           return Status.SMALL
        }
        else{
           return Status.MEDIUM
        }
    }
}

以上代碼通過房子size大小判斷是small還是medium。現在產品經理說咱給他添加一個large的size把。於是你興高采烈改了代碼,簡單的很,不就是加一個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
        }
    }
}

結果app跑起來之後crash了!

仔細一看,原來Activity裏面有這樣的代碼(這裏的例子都只是模擬場景,爲的是說明測試的重要性,現實開發中肯定不可能把這種條件判斷寫在activity裏面)

class HouseActivity extends Activity{
    public void display(int size){
       if(size > 10000){
          throws IllegalStatusException()
       }
       Status status = presenter.getFinancialStatus(size)
       .....其他邏輯
    }
}

HouseActivity 的單元測試長這個樣子:

@Test(expected = IndexOutOfBoundsException.class)
public void sizeTooLargeAssertException(){
    activity.display(30000)
}

原來我們在activity裏面有邏輯,限制最多隻能展示大於10000的size,如果我在運行app之前就已經實現跑過了HouseActivity 的單元測試,我就會提前知道原來我們的app不處理大於10000的數據。

以上只是一個簡單的例子,但是這個例子說明了一個很大的問題,就是在提交你的代碼之前,運行一個有效的單元測試是有多麼重要。他可以幫你測試修改的代碼會對其他模塊有什麼影響,如果破壞了既有的測試(規則),你應該怎麼處理。要知道很多代碼在修改之後,你以爲你打開app手動測試一下通過了肯定就沒問題,但是你有沒有想過,這個代碼,這個類,會不會對其他頁面有影響。這個就是單元測試的作用:

制定一套既有的規則,所有新增/修改的代碼要按照這個規則來運行。

測試這種規則,要比你手動打開app測試更加健壯且快速(compile 一個完整app vs 運行 一個純java的測試)。

在理想狀態下,每一個類的每一行代碼都要被unit test cover,一套單元測試的coverage(覆蓋率)可以體現你給你代碼制定規則的數量和健壯程度。比如說還是用上面的例子:

class Presenter{
    enum Status{
       LARGE,
       MEDIUM
    }
    
    public Status getFinancialStatus(int size){
        if(size < xxx){
           return Status.SMALL
        }
        else{
           return Status.MEDIUM
        }
    }
}

你的測試如果只有:

@Test
public void smallSizeReturnSmallStatus(){
    int size = 90
    
    assertThat(presenter.getFinancialStatus(size)).isEqualTo(Status.SMALL)
}

那你對Presenter這個類的coverage只有50%。爲什麼?因爲你的test沒有覆蓋到else這個語句,補上一下測試:

@Test
public void largeSizeReturnLargeStatus(){
    int size = 3000
    
    assertThat(presenter.getFinancialStatus(size)).isEqualTo(Status.MEDIUM)
}

跑完這兩個測試,你的presener的單元測試覆蓋率就是100%了,恭喜!

順便說一句,現在android studio已經支持顯示unit test 覆蓋率了,有興趣可以看看

https://developer.android.com/studio/test

比如我有個dummy class

給它的else語句加個test:

Android Studio不僅會給出覆蓋率等重要數據,還會給代碼加上標記,這樣開發者就可以輕易的看出來哪一行代碼沒有被測試覆蓋,是否需要加測試(原諒色代表被覆蓋,紅色代表沒有被覆蓋)

測什麼?

我們都知道一個類的單元測試是要保證這個類能正常運行。那麼什麼是類能正常運行呢?這個標準是什麼?

還是以例子爲主:

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 是一個做http call的類,參數是Status。當size小於1000就傳SMALL,反之MEDIUM。那對於HousePagePresenter來說,這個類怎麼樣運行纔是正確的?

那就是當getHouseData() 傳入的參數小於1000的時候,service 類成員要調用call 方法,而且參數是SMALL,反之是MEDIUM。

HousePagePresenter只需要保證在合適的size的前提下,service能調用call並且使用正確的Status就行了。我們只在乎service有沒有做出正確的動作,至於動作結果,不重要!

怎麼測?

那說回來,這個怎麼測?

首先,要給一個代碼做測試,要先保證他是可測的。上面的代碼其實是沒法測試的!Not testable.因爲HouseApiService作爲私有對象,我們沒辦法模擬(Mock)它,從而無法驗證它的行爲在一定條件下是否符合我們的期望。

正確的做法是,要做“依賴注入”。把HousePagePresenter對因爲HouseApiService的依賴,從類對象的方式轉移成別的方式,或者說可測的方式,比如移到構造函數裏面(也可以通過別的方式比如說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)
        }
    }
}

這樣的好處可以說是非常大。這樣,我們在測試HousePagePresenter類的時候,就不需要真正的創建一個HouseApiService了,而是可以模擬:

@Test
public void smallSizeServiceCall(){
    int size = 900
    HouseApiService service = mock(service.class)
    HousePagePresenter presenter = new HousePagePresenter(service)
    
    presenter.getHouseData(size)
    
    //驗證service是不是真正調用了call,並且參數也是期望值
    verify(service).call(Status.SMALL)
}

通過把Service移到構造函數,讓代碼可以通過mockito mock的方式生成一個模擬的Service,這個service不會做任何真正的http call,只會記錄自己call()方法被調用的情況。這就夠了,這已經能證明HousePagePresenter這個類沒問題,如果service有問題,那應該在service自己的單元測試裏面解決。

具體怎麼解決依賴注入,可以稍微看一下一個視頻

https://www.bilibili.com/video/BV1e54y1S72A/?spm_id_from=333.788.recommend_more_video.-1

有人覺得只有用dagger這類依賴注入庫才叫依賴注入,這是一個常見的誤解。想了解更多的朋友可以自行搜索一下。

安卓控件沒法測?

很多朋友會說自己有很多邏輯需要安卓本身的控件支持,這部分真的沒法測啊。乖乖,谷歌已經給我們提供了從UI到系統api的全家桶,想偷懶不寫test?不存在的。。。

純UI的單元測試

對於fragment 和 activity本身的UI測試,Roboletric 框架提供了ActivityRule支持,允許開發者在unit test中啓動測試activity,從而啓動fragment。同時配合Espresso框架可以再unit test代碼中獲取View對象,達到測試View的目的。

比如::

//設置測試activity類
private activityScenarioRule = ActivityScenarioRule(TestActivity.class)
@Before
void setup{
   //啓動測試fragment
   activityScenarioRule.scenario.onActivity{     
      activity.setFragmemnt(new TestFragment());
   }
}

@Test
void whenButtonClicked_executeMethod(){
// 通過onView獲取button,手動模擬點擊事件
   onView(R.id.button).performClick();
   verify(presenter).getHouseData()
}

結合ActivityScenarioRule和Espresso,我們可以把Fragment或者Activity當成一個正常的再正常不過的類來進行測試了。我剛剛入職谷歌的時候就想偷懶不給UI寫test,找藉口說UI測不了,直接被senior大哥焦作人。。。

系統API

假如你的方法裏需要獲得當前手機運營商信息,那你可能需要TelephonyManager這個系統api來幫忙。

fun getCarrierId(){
        val manager: TelephonyManager = applicationContext.getSystemService(TelephonyManager::class.java)
        if(manager.simCarrierId == 1){
            //做什麼邏輯
        }
        else{
            //做其他邏輯
        }
    }

這種情況,你需要Shadow object來幫忙啦!

Roboletric 提供各種系統級別API的shadow,幫助你在測試的時候模擬不同的其情況。

比如:


@Test
fun testCarrierId(){
   val shadowTelephonyManager  = Shadows.of(context.getSystemService(TelephonyManager::class::java))

   //給shadow強行設置一個值
   
   shadowTelephonyManager.setSimCarrierId(-1);
   
   //繼續測試getCarrierId()方法
}

通過shadow,我們就可以測試那些含有系統級別api的類和方法了。

有了Roboletric之後,以前那些複雜的UI,和系統api測試再也不是問題了。我寫了這麼久測試之後發現,基本上沒有不可以shadow,或者不能mock的東西了。每當我發現自己的代碼的某一行測不了,那肯定是我的代碼沒有寫成可測的形式。

結尾

來谷歌的這幾個月可以說我在各種被教做人,知識非常匱乏。谷歌在測試,和代碼規範方面比之前亞麻可以說嚴了不只一個級別,第一個月我就打破了自己修改代碼的記錄,一個PR修改了30次。。。

但是被教做人的同時我也學到了不少,尤其是unit test。以前在亞麻和創業公司隨意慣了,寫測試?不存在的。。。在寫崩組內系統次數逐漸增加之後,我也漸漸意識到了單元測試的重要性,也想着趁腦子還有貨和大家多分享一下,也請各位大牛多多指正!

祝大家五一快樂!羨慕國內的朋友已經到處遊山玩水了。。。美帝疫情還是一天新增好幾萬。。。。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章