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
以上圖片的內容來自文章: ofollow,noindex">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
。這個意思使想要使用但是不被呼叫。
以上的例子就是單元測試的一個簡單的例子了。
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可能會遇到
3.3、靜態類方法無法被Mock
具體可以看看這個。 https://blog.csdn.net/hongchangfirst/article/details/46453677
4、其他的測試
單元測試不僅僅只Presenter測試,還有view層的測試和model層的測試。這裡就不做說明啦,主要是沒去做過view層和model層的測試,知道Presenter層的測試,其他的測試應該大同小異。

手動滑稽