很多人在面試的時候回答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提供的註解和方法,否則就無法進行測試。
比如:
以上圖片的內容來自文章: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)
在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最後會調用的方法。
然後單獨測試這個方法。
如果綠了就成功了。(什麼玩意? 綠..綠了?)
那這裏對返回的參數進行修改,將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()方法一定不會被調用,那麼測試肯定無法通過。那麼運行一下。
好的紅了,看下這個提示信息Wanted but not invoked。這個意思使想要使用但是不被調用。
以上的例子就是單元測試的一個簡單的例子了。
2.1、TTD測試驅動開發模式的單元測試
這裏提到了TTD測試驅動開發,TTD測試驅動開發到底是個什麼東東呢?
在開發之前,先編寫測試代碼,通過測試案例去編寫功能代碼。一開始測試案例由於沒有功能代碼都會無法運行通過,然後通過修改功能代碼使測案例一個一個通過測試。
如果這個功能用TTD測試驅動開發去做如何做呢?
- 首先Presenter中的login()方法不要寫功能代碼,讓他是一個空的方法。
- 然後新建Presenter測試類將測試案例都列舉出來。
- 編寫測試案例的測試代碼。
- 根據測試代碼去編寫功能代碼,使測試案例一個一個通過測試。
3、可能會遇到的問題
3.1、Android的api無法直接使用需要特殊處理
在Presenter中一般只做邏輯操作,不做界面處理,所以很少會用到Android的API。但是這個不是絕對的。比如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層的測試,其他的測試應該大同小異。