Android单元测试(七):MVP与单元测试
本篇介紹如何對(duì)MVP架構(gòu)的項(xiàng)目進(jìn)行單元測(cè)試。會(huì)用到之前六篇文章中所介紹的內(nèi)容,算是學(xué)以致用了。本文中我沒(méi)具體說(shuō)明的地方前幾篇文中一定會(huì)有的。希望大家可以循序漸進(jìn)。
對(duì)于MVP網(wǎng)上也有很多變種,各有千秋,但是萬(wàn)變不離其宗。本篇采用的MVP是《Android源碼設(shè)計(jì)模式解析與實(shí)戰(zhàn)》這本書(shū)中介紹的一種,我也有寫(xiě)過(guò)相關(guān)的讀書(shū)筆記,沒(méi)看過(guò)這本書(shū)的可以簡(jiǎn)單了解下。我本人還是很喜歡這種MVP的。
1.MVP相關(guān)基類(lèi)
View的接口 : MvpView
public interface MvpView {/**** 獲取Context* @return Context*/Context getContext();/**** 顯示Progress*/void showProgress();/**** 關(guān)閉Progress*/void closeProgress();/**** @param string 消息內(nèi)容*/void showToast(String string); }扮演著view和model的中間層的角色 : BaseMVPPresenter
public abstract class BaseMVPPresenter<T extends MvpView> {/*** View接口類(lèi)型的弱引用*/private Reference<T> mViewRef;protected T mMvpView;/*** 建立關(guān)聯(lián)*/public void attachView(T view){mViewRef = new WeakReference<>(view);if(isViewAttached()) {mMvpView = getView();}}/*** 獲取View* @return View*/public T getView(){return mViewRef.get();}/*** UI展示相關(guān)的操作需要判斷一下 Activity 是否已經(jīng) finish.* <p>* todo : 只有當(dāng) isActivityAlive 返回true時(shí)才可以執(zhí)行與Activity相關(guān)的操作,* 比如 彈出Dialog、Window、跳轉(zhuǎn)Activity等操作.** @return boolean*/public boolean isViewAttached(){return mViewRef != null && mViewRef.get() != null;}/*** 解除關(guān)聯(lián)*/public void detachView(){if( mViewRef != null){mViewRef.clear();mViewRef = null;}} }簡(jiǎn)單封裝的view : BaseMVPActivity
public abstract class BaseMVPActivity<V extends MvpView, T extends BaseMVPPresenter<V>> extends AppCompatActivity implements MvpView{/*** Presenter對(duì)象*/protected T mPresenter;public ProgressDialog mProgress;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);mPresenter = createPresenter();mPresenter.attachView((V)this);mProgress = new ProgressDialog(this);mProgress.setMessage("加載中...");}@Overrideprotected void onDestroy() {if (mPresenter != null){mPresenter.detachView();}super.onDestroy();}@Overrideprotected void onRestoreInstanceState(Bundle savedInstanceState) {super.onRestoreInstanceState(savedInstanceState);if (mPresenter == null){mPresenter = createPresenter();}}/*** 創(chuàng)建Presenter對(duì)象* @return Presenter對(duì)象*/protected abstract T createPresenter();@Overridepublic Context getContext() {return this;}@Overridepublic void showProgress() {if (mProgress != null && !mProgress.isShowing()){mProgress.show();}}@Overridepublic void closeProgress() {if (mProgress != null && mProgress.isShowing()) {mProgress.dismiss();}}@Overridepublic void showToast(String string) {Toast.makeText(this, string, Toast.LENGTH_SHORT).show();} }2.舉栗子
這次我們還是采用上一篇的例子。一個(gè)簡(jiǎn)單的登錄頁(yè)面,其中有兩個(gè)功能:
獲取驗(yàn)證碼(點(diǎn)擊獲取驗(yàn)證碼后,實(shí)現(xiàn)一個(gè)120s的倒計(jì)時(shí))
登錄(驗(yàn)證輸入的手機(jī)號(hào)碼與驗(yàn)證碼,請(qǐng)求登錄接口)
代碼很簡(jiǎn)單,我一一的貼出來(lái):
public interface LoginMvpView extends MvpView{/*** 倒計(jì)時(shí)完成*/void countdownComplete();/*** 倒計(jì)時(shí)中* @param time 剩余時(shí)間*/void countdownNext(String time);/*** 登錄成功*/void loginSuccess();} public class LoginPresenter extends BaseMVPPresenter<LoginMvpView>{private CompositeDisposable mCompositeDisposable = new CompositeDisposable();public void getIdentify() {// interval隔一秒發(fā)一次,到120結(jié)束Disposable mDisposable = Observable.interval(1, TimeUnit.SECONDS, AndroidSchedulers.mainThread()).take(120).subscribeWith(new DisposableObserver<Long>() {@Overridepublic void onComplete() {mMvpView.countdownComplete();}@Overridepublic void onError(Throwable e) {mMvpView.showToast("倒計(jì)時(shí)出現(xiàn)錯(cuò)誤!");}@Overridepublic void onNext(Long aLong) {mMvpView.countdownNext(String.valueOf(Math.abs(aLong - 120)));}});mCompositeDisposable.add(mDisposable);}public void login(String mobile, String code) {if(mobile.length() != 11){mMvpView.showToast("手機(jī)號(hào)碼不正確");return;}if(code.length() != 6){mMvpView.showToast("驗(yàn)證碼不正確");return;}GithubService.createGithubService().getUser("simplezhli").subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).doOnSubscribe(new Consumer<Disposable>() {@Overridepublic void accept(Disposable disposable) throws Exception {if (isViewAttached()){mMvpView.showProgress();}}}).doAfterTerminate(new Action() {@Overridepublic void run() throws Exception {if (isViewAttached()){mMvpView.closeProgress();}}}).subscribe(new Observer<User>() {@Overridepublic void onSubscribe(Disposable d) {mCompositeDisposable.add(d);}@Overridepublic void onNext(User user) {mMvpView.showToast("登錄成功");mMvpView.loginSuccess();}@Overridepublic void onError(Throwable e) {mMvpView.showToast("登錄失敗");}@Overridepublic void onComplete() {}});}@Overridepublic void detachView(){super.detachView();mCompositeDisposable.clear();}} public class LoginMVPActivity extends BaseMVPActivity<LoginMvpView, LoginPresenter> implements LoginMvpView, View.OnClickListener{private TextView mTvSendIdentify;private EditText mEtMobile;private EditText mEtIdentify;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_login);mEtMobile = (EditText) this.findViewById(R.id.et_mobile);mEtIdentify = (EditText) this.findViewById(R.id.et_identify);mTvSendIdentify = (TextView) this.findViewById(R.id.tv_send_identify);this.findViewById(R.id.tv_login).setOnClickListener(this);mTvSendIdentify.setOnClickListener(this);}@Overrideprotected LoginPresenter createPresenter() {return new LoginPresenter();}@Overridepublic void countdownComplete() {mTvSendIdentify.setText(R.string.login_send_identify);mTvSendIdentify.setEnabled(true);}@Overridepublic void countdownNext(String time) {mTvSendIdentify.setText(TextUtils.concat(time, "秒后重試"));}@Overridepublic void loginSuccess() {showToast("登錄成功");}@Overridepublic void onClick(View view) {switch (view.getId()){case R.id.tv_send_identify:mTvSendIdentify.setEnabled(false);mPresenter.getIdentify();break;case R.id.tv_login:mPresenter.login(mEtMobile.getText().toString().trim(),mEtIdentify.getText().toString().trim());break;default:break;}} }實(shí)現(xiàn)代碼很簡(jiǎn)單,我就不具體說(shuō)明了,主要說(shuō)說(shuō)單元測(cè)試部分。
3.單元測(cè)試
單元測(cè)試主要測(cè)試兩部分:Activity與Presenter。
Activity部分其實(shí)和上一篇大同小異,主要是測(cè)試界面上的View的狀態(tài)變化和文字顯示,Toast, Dialog的彈出與顯示內(nèi)容這些是否符合預(yù)期。
Presenter 部分測(cè)試數(shù)據(jù)處理的正確性,回調(diào)接口的次數(shù)與內(nèi)容是否符合預(yù)期。
Activity測(cè)試部分代碼:
@RunWith(RobolectricTestRunner.class) @Config(constants = BuildConfig.class, sdk = 23) public class LoginMVPActivityTest {private LoginMVPActivity loginActivity;private TextView mTvSendIdentify;private TextView mTvLogin;private EditText mEtMobile;private EditText mEtIdentify;@Rulepublic RxJavaTestSchedulerRule rule = new RxJavaTestSchedulerRule();@Beforepublic void setUp(){ShadowLog.stream = System.out;loginActivity = Robolectric.setupActivity(LoginMVPActivity.class);mTvSendIdentify = (TextView) loginActivity.findViewById(R.id.tv_send_identify);mTvLogin = (TextView) loginActivity.findViewById(R.id.tv_login);mEtMobile = (EditText) loginActivity.findViewById(R.id.et_mobile);mEtIdentify = (EditText) loginActivity.findViewById(R.id.et_identify);}@Testpublic void testGetIdentify() throws Exception {Application application = RuntimeEnvironment.application;assertEquals(mTvSendIdentify.getText().toString(),application.getString(R.string.login_send_identify));// 觸發(fā)按鈕點(diǎn)擊mTvSendIdentify.performClick();// 時(shí)間到10秒rule.getTestScheduler().advanceTimeTo(10, TimeUnit.SECONDS);assertEquals(mTvSendIdentify.isEnabled(), false);assertEquals(mTvSendIdentify.getText().toString(), "111秒后重試");// 時(shí)間到120秒rule.getTestScheduler().advanceTimeTo(120, TimeUnit.SECONDS);assertEquals(mTvSendIdentify.getText().toString(),application.getString(R.string.login_send_identify));assertEquals(mTvSendIdentify.isEnabled(), true);}@Testpublic void testLogin() throws Exception {mEtMobile.setText("123");mEtIdentify.setText("123");mTvLogin.performClick();assertEquals("手機(jī)號(hào)碼不正確", ShadowToast.getTextOfLatestToast());mEtMobile.setText("13000000000");mEtIdentify.setText("123");mTvLogin.performClick();assertEquals("驗(yàn)證碼不正確", ShadowToast.getTextOfLatestToast());initRxJava();mEtMobile.setText("13000000000");mEtIdentify.setText("123456");mTvLogin.performClick();// 判斷ProgressDialog彈出assertNotNull(ShadowProgressDialog.getLatestDialog());assertEquals("登錄成功", ShadowToast.getTextOfLatestToast());}private void initRxJava() {RxJavaPlugins.reset();RxJavaPlugins.setIoSchedulerHandler(new Function<Scheduler, Scheduler>() {@Overridepublic Scheduler apply(Scheduler scheduler) throws Exception {return Schedulers.trampoline();}});RxAndroidPlugins.reset();RxAndroidPlugins.setMainThreadSchedulerHandler(new Function<Scheduler, Scheduler>() {@Overridepublic Scheduler apply(Scheduler scheduler) throws Exception {return Schedulers.trampoline();}});}}Presenter測(cè)試部分代碼:
@RunWith(RobolectricTestRunner.class) @Config(constants = BuildConfig.class, sdk = 23) public class LoginPresenterTest{private LoginPresenter mPresenter;@Mockprivate LoginMvpView mvpView;@Rulepublic MockitoRule mockitoRule = MockitoJUnit.rule();@Rulepublic RxJavaTestSchedulerRule rule = new RxJavaTestSchedulerRule();@Beforepublic void setUp(){//輸出日志ShadowLog.stream = System.out;mPresenter = new LoginPresenter();mPresenter.attachView(mvpView);}@Testpublic void testGetIdentify() throws Exception {mPresenter.getIdentify();// 時(shí)間到10秒rule.getTestScheduler().advanceTimeTo(10, TimeUnit.SECONDS);// 驗(yàn)證方法被調(diào)用10次verify(mvpView, times(10)).countdownNext(anyString());// 時(shí)間到120秒rule.getTestScheduler().advanceTimeTo(120, TimeUnit.SECONDS);verify(mvpView, times(120)).countdownNext(anyString());// 驗(yàn)證倒計(jì)時(shí)完成方法被調(diào)用verify(mvpView).countdownComplete();}@Testpublic void testLogin() throws Exception {initRxJava();mPresenter.login("123", "123");verify(mvpView).showToast("手機(jī)號(hào)碼不正確");mPresenter.login("13000000000", "123");verify(mvpView).showToast("驗(yàn)證碼不正確");mPresenter.login("13000000000", "123456");verify(mvpView).showProgress();verify(mvpView).loginSuccess();verify(mvpView).closeProgress();}private void initRxJava() {RxJavaPlugins.reset();RxJavaPlugins.setIoSchedulerHandler(new Function<Scheduler, Scheduler>() {@Overridepublic Scheduler apply(Scheduler scheduler) throws Exception {return Schedulers.trampoline();}});RxAndroidPlugins.reset();RxAndroidPlugins.setMainThreadSchedulerHandler(new Function<Scheduler, Scheduler>() {@Overridepublic Scheduler apply(Scheduler scheduler) throws Exception {return Schedulers.trampoline();}});} }本篇內(nèi)容不多,主要是前面講解內(nèi)容的一個(gè)整合,下一篇會(huì)說(shuō)說(shuō)MVP結(jié)合Dagger的單元測(cè)試。所有代碼已上傳至Github。希望大家多多點(diǎn)贊支持!
總結(jié)
以上是生活随笔為你收集整理的Android单元测试(七):MVP与单元测试的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 四年级计算机课程表制作教案,四年级信息技
- 下一篇: 罗斯蒙特流量计调试需要考虑的两大方面