Android單元測試—— MVP中的Presenter測試

很多人在面試的時候回答MVP的優點會提出:“有利於單元測試”。但是很多程序員沒有寫單元測試的習慣,特別是小型的創業公司,由於大量的編碼工作使程序員將測試的任務全部交給了測試部門。實際上單元測試能夠減少邏輯上的錯誤和bug量。

1、Presenter中的邏輯測試

這裏只對Presenter的測試進行說明,Presenter的測試相對於Mode和View的測試更加重要。因爲主要的邏輯代碼寫在這裏。

1.1、Presenter測試的邏輯原理

在測試代碼中編寫一些測試案例,斷言可能出現的結果來測試功能代碼的邏輯。在MVP架構中,將View和Model分開了,用Presenter進行View和Model通信。用斷言的方式來列舉出Model可能出現的數據,驗證Presenter中的邏輯判斷,然後檢查View層執行的方法。
其中:測試的過程控制、測試邏輯、測試案例等用到Junit框架;用於斷言和檢查方法執行用到Mockito框架。

1.2、單元測試依賴的測試框架

Presenter爲邏輯測試,主要依賴Junit和mokito框架。

    // 測試相關
    testImplementation "junit:junit:4.12"
    testImplementation "org.mockito:mockito-core:1.10.19"

在進行單元測試前,必須瞭解Junit和Mockito提供的註解和方法,否則就無法進行測試。
比如:

image.png

以上圖片的內容來自文章:https://www.jianshu.com/p/5c8cde7ab54e
如果不知道Junit和mokito,建議先點過去看下。

2、在MVP框架中的單元測試實戰

假如現在在做一個登錄的功能,登錄的需求如下。

  • 1、登錄成功。(當服務返回的code爲0。)
  • 2、登錄失敗。(當服務其返回的code不爲0。)
  • 3、登錄失敗,因爲用戶名爲空。
  • 4、登錄失敗,因爲密碼爲空。
2.1、非TTD測試驅動開發模式的單元測試

(什麼是TTD測試驅動開發?後面會提到)
按照需求,會去創建MVP結構來做這個模塊。(注意:1、這裏展示的是最簡單的MVP結構。2、網絡請求框架我用的是已經封裝好的Rxjava+Retorfit。3、api用的是WanAndroid的api)

image.png

在LoginContract中根據需求定義接口:

public interface LoginContract {
    public interface View {
        // 登錄成功。(當服務返回的code爲0。)
        void loginSuccess();

        // 登錄失敗。(當服務其返回的code不爲0。)
        void loginFail();

        // 登錄失敗,因爲用戶名錯誤
        void loginFailCauseByErrorUserName();

        // 登錄失敗,因爲用戶名錯誤
        void loginFailCauseByErrorPassword();
    }

    public interface Model {
        Observable<Response<JSONObject>> login(String userName, String userPwd);
    }

    public interface Presenter {
        void login(String userName, String userPwd);
    }
}

Presenter中的代碼:

/**
 * 登錄Presenter層
 */
public class LoginPresenter implements LoginContract.Presenter {

    private LoginContract.Model model;
    private LoginContract.View view;
    private BaseSchedulerProvider schedulerProvider;
    private CompositeDisposable mDisposable;

    public LoginPresenter(LoginContract.Model model,LoginContract.View view){
        this.view = view;
        this.model = model;
        mDisposable = new CompositeDisposable();
        schedulerProvider = SchedulerProvider.getInstance();
    }

    /**
     * 這個是爲單元測試建的構造函數,原因是因爲Rxjava線程切換,必須設置爲立即執行才能測試通過
     * @param model
     * @param view
     * @param schedulerProvider
     */
    public LoginPresenter(LoginContract.Model model,LoginContract.View view,SchedulerTestProvider schedulerProvider){
        this.view = view;
        this.model = model;
        mDisposable = new CompositeDisposable();
        this.schedulerProvider = schedulerProvider;
    }

    @Override
    public void login(String userName, String userPwd) {
        if (TextUtils.isEmpty(userName)) {
            view.loginFailCauseByErrorUserName();
            return;
        }
        if (TextUtils.isEmpty(userPwd)) {
            view.loginFailCauseByErrorPassword();
            return;
        }
        Disposable disposable = model.login(userName, userPwd).
                compose(ResponseTransformer.handleResult()).
                compose(schedulerProvider.applySchedulers())
                .subscribe(jsonObject -> {
                            view.loginSuccess();
                        },
                        throwable -> {
                            view.loginFail();
                        });
        mDisposable.add(disposable);
    }
}

這裏要進行測試的就是login(String userName, String userPwd)方法中的邏輯。測試的時候會斷言服務器返回的數據,會列舉一些登錄成功和登錄失敗的測試案例,如果這些測試案例能夠正常運行,那麼就代表這些代碼的邏輯運行正常。

  • 新建一個測試類去測試Presenter的邏輯。


    新建測試類
  • 根據需求寫出測試案例初始化Presenter使用的必要類

/**
 * 登錄Presenter的測試類
 */
public class LoginPresenterTest {

    @Mock
    private LoginContract.Model model;

    @Mock
    private LoginContract.View view;

    private LoginPresenter presenter;

    @Mock
    private SchedulerTestProvider schedulerProvider;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        schedulerProvider = new SchedulerTestProvider();
        presenter = new LoginPresenter(model, view,schedulerProvider);
    }

    /**
     * 登陸成功
     * @throws Exception
     */
    @Test
    public void loginSuccess() throws Exception {
    }

    /**
     * 登錄失敗,服務器返回錯誤的code
     * @throws Exception
     */
    @Test
    public void loginFailByServer() throws Exception{
    }

    /**
     * 登錄失敗,錯誤的用戶名
     * @throws Exception
     */
    @Test
    public void loginFailByErrorUserName() throws Exception{
    }

    /**
     * 登錄失敗,錯誤的密碼
     * @throws Exception
     */
    @Test
    public void loginFailByErrorPwd() throws Exception{
    }

}

這裏寫出登錄成功loginsuccess()的測試代碼:

    @Test
    public void loginSuccess() throws Exception {
        // 1、斷言model.login的方法返回正確的Response數據
        when(model.login("123321","123321")).thenReturn(Observable.just(new Response<>(0,new JSONObject(),"")));
        // 2、Presenter執行登錄邏輯
        presenter.login("123321","123321");
        // 3、預測回調給view層的方法是否被調用
        verify(view).loginSuccess();
    }

這裏的測試邏輯爲:
1、斷言model返回正確的數據。
2、presenter去調用登錄的方法。
3、檢測view最後會調用的方法。

然後單獨測試這個方法。


點這裏測試

如果綠了就成功了。(什麼玩意? 綠..綠了?)


image.png

那這裏對返回的參數進行修改,將code改爲非0,這個邏輯在網絡請求框架中的ResponseTransformer類中定義的,非0則請求失敗。

    @Test
    public void loginSuccess() throws Exception {
        // 1、斷言model.login的方法返回正確的Response數據
        when(model.login("123321","123321")).thenReturn(Observable.just(new Response<>(1,new JSONObject(),"")));
        // 2、Presenter執行登錄邏輯
        presenter.login("123321","123321");
        // 3、預測回調給view層的方法是否被調用
        verify(view).loginSuccess();
    }

按照實際邏輯,如果爲非0就是登錄失敗,view層的loginSuccess()方法一定不會被調用,那麼測試肯定無法通過。那麼運行一下。

TIM截圖20181013144539.png

好的紅了,看下這個提示信息Wanted but not invoked。這個意思使想要使用但是不被調用。

以上的例子就是單元測試的一個簡單的例子了。

2.1、TTD測試驅動開發模式的單元測試

這裏提到了TTD測試驅動開發,TTD測試驅動開發到底是個什麼東東呢?

在開發之前,先編寫測試代碼,通過測試案例去編寫功能代碼。一開始測試案例由於沒有功能代碼都會無法運行通過,然後通過修改功能代碼使測案例一個一個通過測試。

如果這個功能用TTD測試驅動開發去做如何做呢?

  • 首先Presenter中的login()方法不要寫功能代碼,讓他是一個空的方法。
  • 然後新建Presenter測試類將測試案例都列舉出來。
  • 編寫測試案例的測試代碼。
  • 根據測試代碼去編寫功能代碼,使測試案例一個一個通過測試。

3、可能會遇到的問題

3.1、Android的api無法直接使用需要特殊處理

在Presenter中一般只做邏輯操作,不做界面處理,所以很少會用到Android的API。但是這個不是絕對的。比如TextUtils做非空判斷。如果不對這個做特殊處理,那麼會報無法調用的錯誤。
解決方案:在TextUtils中新建一個包名一樣的TextUtils類即可。

新建TextUtils
3.2、Rxjava的線程測試處理,這裏用到Rxjava可能會遇到

這個需要將線程的改爲Schedulers.trampoline()強制立即進行當前的任務。
具體看下這篇文章吧:
https://www.jianshu.com/p/22384556bd22

3.3、靜態類方法無法被Mock

具體可以看看這個。https://blog.csdn.net/hongchangfirst/article/details/46453677

4、其他的測試

單元測試不僅僅只Presenter測試,還有view層的測試和model層的測試。這裏就不做說明啦,主要是沒去做過view層和model層的測試,知道Presenter層的測試,其他的測試應該大同小異。


手動滑稽

代碼地址:https://github.com/AxeChen/MvpUnitTest

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