Android 單元測試實踐

pqpo發表於2019-03-03

如果想看更多文章可以到我的部落格,部落格會先更新:
Android 單元測試實踐

單元測試是什麼

單元測試 是針對 程式的最小單元 來進行正確性檢驗的測試工作。程式單元是應用的最小可測試部件。一個單元可能是單個程式、類、物件、方法等。 ——維基百科複製程式碼

為什麼要做單元測試

賣個關子,看完文章自然就知道了複製程式碼

原來和很多人一樣並沒有寫單元測試的習慣,寫好一個功能模組之後直接在真機上做自測,看看剛寫的功能是否和預期一致,如果不一致,從頭debug找問題出在哪兒,沒問題就提交測試,測試測出問題,再從頭debug找問題出在哪兒。這個過程一般會比較費時,但一直以來都這麼幹,也沒發現有什麼問題。

後來看到一些安利單元測試的文章,被洗腦似的決定開始寫單元測試。

然後,就沒有然後了。

原來所有的程式碼邏輯都在Activity裡,如何寫單元測試?瞬間懵逼了。

很多公司或個人不願意寫單元測試的原因可能是覺得寫單元測試並沒用有什麼卵用,專案比較趕根本沒有時間寫單元測試,不知道從何下手,特別是 Android 應用更是比較難寫單元測試。

後來看到有文章說使用MVP模式可以方便的寫單元測試,而且可以使用Junit寫單元測試,直接執行在JVM上,而不需要執行在Android環境中。

然後就有了這篇文章《如何將原專案重構成MVP模式》

開始實踐

寫了這麼多鋪墊,終於可以開始操刀寫單元測試了,各位看官是不是已經急不可耐了。

就拿這個類開始寫單元測試吧:

public class CreditCardPresenter extends BasePresenter<CreditCardContract.View, CreditCardContract.Model> implements CreditCardContract.Presenter {

    //其他程式碼略
    public void getCreditCards() {
        getModel().getCreditCards()
                .subscribe(new Subscriber<List<CreditCard>>(){
                    @Overridec
                    public void onNext(List<CreditCard> creditCards) {
                        getView().showCreditCards(creditCards);
                    }

                    @Override
                    public void onCompleted() {
                        getView().loadCompleted();
                    }

                    @Override
                    public void onError(Throwable e) {
                        getView().showError(e);
                    }

                });
    }
}複製程式碼

功能很簡單,就是獲取信用卡列表,如果獲取成功就通過下面的程式碼顯示:

getView().showCreditCards(creditCards);
getView().loadCompleted();複製程式碼

如果出錯,則通知頁面顯示錯誤資訊:

getView().showError(e);複製程式碼

又懵逼了,getModel() 和 getView() 裡面還是會呼叫安卓的程式碼,怎麼使用Junit做測試呢?

引入一個強大的測試框架:Mockito,接下來就可以開始使用Junit & Mockito做Java程式碼的單元測試了,這種方式的單元測試可以直接執行與JVM上,使用Mockito隔離Android相關程式碼。

然後就可以為CreditCardPresenter 寫單元測試了,為了方便,靜態匯入了Mockito的所有方法:

import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class CreditCardPresenterTest {

    CreditCardPresenter creditCardPresenter;
    @Mock
    CreditCardContract.View creditCardView;
    @Mock
    CreditCardContract.Model creditCardModel;

    List<CreditCard> creditCards;

    @Before
    public void setUp() throws Exception {
        creditCardPresenter = new CreditCardPresenter();
        creditCardPresenter.attachView(creditCardView);
        creditCardPresenter.setModel(creditCardModel);
        creditCards = new ArrayList<>();
    }

    public void testGetCreditCards() {
        when(creditCardModel.getCreditCards()).thenReturn(Observable.create(new Observable.OnSubscribe<List<CreditCard>>() {
            @Override
            public void call(Subscriber<? super List<CreditCard>> subscriber) {
                subscriber.onNext(creditCards);
                subscriber.onCompleted();
            }
        }));

        creditCardPresenter.getCreditCards();

        verify(creditCardView).showCreditCards(creditCards);
        verify(creditCardView).loadCompleted();
    }

    public void testGetCreditCardsOnError() {
        final RuntimeException exception = new RuntimeException();
        when(creditCardModel.getCreditCards()).thenReturn(Observable.create(new Observable.OnSubscribe<List<CreditCard>>() {
            @Override
            public void call(Subscriber<? super List<CreditCard>> subscriber) {
                throw exception;
            }
        }));

        creditCardPresenter.getCreditCards();

        verify(creditCardView).showError(exception);
    }

}複製程式碼

這樣就為上述兩種情況寫了兩個單元測試。
其中使用@Mock註解來生成mock物件,也可以setUp方法中使用Mockito.mock()來生成mock物件,當使用註解的時候在類上必須加上註解@RunWith(MockitoJUnitRunner.class)
mock出來的物件的方法都是空實現,void方法宣告也不做,有返回值的方法返回null(int 型別返回0,boolean型別返回false等)。

然後我們可以通過when(…).thenReturn(…)來為mock物件實現方法返回值。

when(creditCardModel.getCreditCards()).thenReturn(Observable.create(new Observable.OnSubscribe<List<CreditCard>>() {
            @Override
            public void call(Subscriber<? super List<CreditCard>> subscriber) {
                subscriber.onNext(creditCards);
                subscriber.onCompleted();
            }
        }));複製程式碼

上面程式碼的意思就是說當呼叫creditCardModel.getCreditCards()的時候返回值是:

Observable.create(new Observable.OnSubscribe<List<CreditCard>>() {
            @Override
            public void call(Subscriber<? super List<CreditCard>> subscriber) {
                subscriber.onNext(creditCards);
                subscriber.onCompleted();
            }
        })複製程式碼

最後使用verify()方法來校驗某個方法是否被執行:

verify(creditCardView).showCreditCards(creditCards);複製程式碼

上面的程式碼意思就是說 creditCardView.showCreditCards(creditCards)方法被執行了,並且引數是creditCards,並且只執行了一次。如果有一個條件不符合就會報測試失敗。
verify()還有很多過載方法,預設其實是這樣的 verfy(creditCardView, times(1)).showCreditCards(creditCards); 校驗只執行了一次,times(1) 可以傳入不同的引數來校驗方法被執行了幾次。還可以替換了nerver(),表示某方法一次也不執行。
當然Mockito的功能遠不止這麼點,還有很多高階用法就不繼續介紹了。
Mockito也有一些美中不足之處,不能mock靜態方法,final方法等,比如專案中會有這樣的方法 SelfApplication.getContext() 來獲取自定義的Application,如果在測試程式碼中出現這類程式碼肯定會測試失敗,因為JVM環境中沒有Application,怎麼辦呢?
再引入一個配合Mockito使用的庫:PowerMock
他彌補了Mockito的不足,可以mock靜態方法和final方法,可以使用PowerMock來mock出SelfApplication.getContext(),從而不會呼叫到真正的Application物件:

PowerMockito.mockStatic(SelfApplication.class);
PowerMockito.when(SelfApplication.getContext()).thenReturn(mock(SelfApplication.class));複製程式碼

另外,在方法上要宣告@PrepareForTest(SelfApplication.class), 在類上要宣告 @RunWith(PowerMockRunner.class) 來支援上述mock。
這樣當 呼叫SelfApplication.getContext()的時候將拿到一個mock物件,我們就可以繼續使用when().thenReturn()方法來處理方法返回值了。
具體關於Mockito 和 PowerMock 的更多用法這裡就不做過多介紹了,官網才是最好的教程。
這只是一個簡單的例子,實際專案中會出現好的複雜的情況。這就要求我寫的程式碼方法要短,耦合要低。寫單元測試逼迫我們寫更優雅的程式碼,也為我們下次修改需求或者重構程式碼提供了一道安全保障。還有其他更多的好處大家自己在實踐中體會吧。

相關文章