Android unit testing (7): MVP and unit testing

Android unit testing (7): MVP and unit testing

This article introduces how to unit test the MVP architecture project. I will use the content introduced in the previous six articles, so I can apply what I have learned. Where I did not specify in this article, there must be some in the previous articles. I hope everyone can make progress step by step.

There are many variants of MVP online, each with its own merits, but they are inseparable. The MVP used in this article is one introduced in the book "Analysis and Actual Combat of Android Source Code Design Patterns". I have also written related reading notes. If you haven't read this book, you can understand it briefly. . I still like this kind of MVP.

1. MVP related base class

View interface: MvpView

public interface MvpView {

   /***
     *  Context
     * @return Context
     */
    Context getContext();

   /***
     *  Progress
     */
    void showProgress();

   /***
     *  Progress
     */
    void closeProgress();

   /***
     * @param string  
     */
    void showToast(String string);
}
 

Plays the role of the middle layer of view and model: BaseMVPPresenter

public abstract class BaseMVPPresenter<T extends MvpView> {

   /**
     * View 
     */
    private Reference<T> mViewRef;

    protected T mMvpView;

   /**
     *  
     */
    public void attachView(T view){
        mViewRef = new WeakReference<>(view);
        if(isViewAttached()) {
            mMvpView = getView();
        }
    }

   /**
     *  View
     * @return View
     */
    public T getView(){
        return mViewRef.get();
    }

   /**
     * UI  Activity   finish.
     * <p>
     * todo :   isActivityAlive  true Activity ,
     *    Dialog Window Activity .
     *
     * @return boolean
     */
    public boolean isViewAttached(){
        return mViewRef != null && mViewRef.get() != null;
    }

   /**
     *  
     */
    public void detachView(){
        if( mViewRef != null){
            mViewRef.clear();
            mViewRef = null;
        }
    }
}
 

Simple packaged view: BaseMVPActivity

public abstract class BaseMVPActivity<V extends MvpView, T extends BaseMVPPresenter<V>> extends AppCompatActivity implements MvpView{

   /**
     * Presenter 
     */
    protected T mPresenter;
    public ProgressDialog mProgress;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mPresenter = createPresenter();
        mPresenter.attachView((V)this);
        mProgress = new ProgressDialog(this);
        mProgress.setMessage(" ...");
    }

    @Override
    protected void onDestroy() {
        if (mPresenter != null){
            mPresenter.detachView();
        }
        super.onDestroy();
    }

    @Override
    protected void onRestoreInstanceState(Bundle savedInstanceState) {
        super.onRestoreInstanceState(savedInstanceState);
        if (mPresenter == null){
            mPresenter = createPresenter();
        }
    }

   /**
     *  Presenter 
     * @return Presenter 
     */
    protected abstract T createPresenter();

    @Override
    public Context getContext() {
        return this;
    }

    @Override
    public void showProgress() {
        if (mProgress != null && !mProgress.isShowing()){
            mProgress.show();
        }
    }

    @Override
    public void closeProgress() {
        if (mProgress != null && mProgress.isShowing()) {
            mProgress.dismiss();
        }
    }

    @Override
    public void showToast(String string) {
        Toast.makeText(this, string, Toast.LENGTH_SHORT).show();
    }
}
 

2. Raise chestnuts

This time we will use the example from the previous article. A simple login page with two functions:

  • Get the verification code (after clicking to get the verification code, a 120s countdown will be realized)

  • Login (verify the entered mobile phone number and verification code, request login interface)

The code is very simple, I posted them one by one:

public interface LoginMvpView extends MvpView{

   /**
     *  
     */
    void countdownComplete();

   /**
     *  
     * @param time  
     */
    void countdownNext(String time);

   /**
     *  
     */
    void loginSuccess();

}
 
public class LoginPresenter extends BaseMVPPresenter<LoginMvpView>{

    private CompositeDisposable mCompositeDisposable = new CompositeDisposable();

    public void getIdentify() {
       //interval 120 
        Disposable mDisposable = Observable
                .interval(1, TimeUnit.SECONDS, AndroidSchedulers.mainThread())
                .take(120)
                .subscribeWith(new DisposableObserver<Long>() {
                    @Override
                    public void onComplete() {
                        mMvpView.countdownComplete();
                    }
                    @Override
                    public void onError(Throwable e) {
                        mMvpView.showToast(" ");
                    }

                    @Override
                    public 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(" ");
            return;
        }
        if(code.length() != 6){
            mMvpView.showToast(" ");
            return;
        }

        GithubService.createGithubService()
                .getUser("simplezhli")
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .doOnSubscribe(new Consumer<Disposable>() {
                    @Override
                    public void accept(Disposable disposable) throws Exception {
                        if (isViewAttached()){
                            mMvpView.showProgress();
                        }

                    }
                })
                .doAfterTerminate(new Action() {
                    @Override
                    public void run() throws Exception {
                        if (isViewAttached()){
                            mMvpView.closeProgress();
                        }
                    }
                })
                .subscribe(new Observer<User>() {
                    @Override
                    public void onSubscribe(Disposable d) {
                        mCompositeDisposable.add(d);
                    }

                    @Override
                    public void onNext(User user) {
                        mMvpView.showToast(" ");
                        mMvpView.loginSuccess();
                    }

                    @Override
                    public void onError(Throwable e) {
                        mMvpView.showToast(" ");
                    }

                    @Override
                    public void onComplete() {}
                });
    }

    @Override
    public 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;

    @Override
    protected 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);
    }

    @Override
    protected LoginPresenter createPresenter() {
        return new LoginPresenter();
    }

    @Override
    public void countdownComplete() {
        mTvSendIdentify.setText(R.string.login_send_identify);
        mTvSendIdentify.setEnabled(true);
    }

    @Override
    public void countdownNext(String time) {
        mTvSendIdentify.setText(TextUtils.concat(time, " "));
    }

    @Override
    public void loginSuccess() {
        showToast(" ");
    }

    @Override
    public 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;
        }
    }
}
 

The implementation code is very simple, so I won't explain it in detail, but mainly talk about the unit test part.

3. Unit testing

The unit test mainly tests two parts: Activityand Presenter.

  • ActivityThe part is actually the same as the previous one, mainly to test whether Viewthe status changes and text display on the interface Toast, Dialogthe pop-up and the displayed content are in line with expectations.

  • Presenter Part of the test data processing correctness, whether the times and content of the callback interface meet expectations.

ActivityTest part of the code:

@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;

    @Rule
    public RxJavaTestSchedulerRule rule = new RxJavaTestSchedulerRule();

    @Before
    public 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);
    }

    @Test
    public void testGetIdentify() throws Exception {
        Application application = RuntimeEnvironment.application;
        assertEquals(mTvSendIdentify.getText().toString(),
                application.getString(R.string.login_send_identify));

       // 
        mTvSendIdentify.performClick();
       // 10 
        rule.getTestScheduler().advanceTimeTo(10, TimeUnit.SECONDS);
        assertEquals(mTvSendIdentify.isEnabled(), false);
        assertEquals(mTvSendIdentify.getText().toString(), "111 ");

       // 120 
        rule.getTestScheduler().advanceTimeTo(120, TimeUnit.SECONDS);

        assertEquals(mTvSendIdentify.getText().toString(),
                application.getString(R.string.login_send_identify));
        assertEquals(mTvSendIdentify.isEnabled(), true);
    }

    @Test
    public void testLogin() throws Exception {

        mEtMobile.setText("123");
        mEtIdentify.setText("123");
        mTvLogin.performClick();
        assertEquals(" ", ShadowToast.getTextOfLatestToast());

        mEtMobile.setText("13000000000");
        mEtIdentify.setText("123");
        mTvLogin.performClick();
        assertEquals(" ", 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>() {
            @Override
            public Scheduler apply(Scheduler scheduler) throws Exception {
                return Schedulers.trampoline();
            }
        });
        RxAndroidPlugins.reset();
        RxAndroidPlugins.setMainThreadSchedulerHandler(new Function<Scheduler, Scheduler>() {
            @Override
            public Scheduler apply(Scheduler scheduler) throws Exception {
                return Schedulers.trampoline();
            }
        });
    }

} 

PresenterTest part of the code:

@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 23)
public class LoginPresenterTest{

    private LoginPresenter mPresenter;

    @Mock
    private LoginMvpView mvpView;

    @Rule
    public MockitoRule mockitoRule = MockitoJUnit.rule();

    @Rule
    public RxJavaTestSchedulerRule rule = new RxJavaTestSchedulerRule();

    @Before
    public void setUp(){
       //
        ShadowLog.stream = System.out;

        mPresenter = new LoginPresenter();
        mPresenter.attachView(mvpView);
    }

    @Test
    public void testGetIdentify() throws Exception {
        mPresenter.getIdentify();
       // 10 
        rule.getTestScheduler().advanceTimeTo(10, TimeUnit.SECONDS);
       // 10 
        verify(mvpView, times(10)).countdownNext(anyString());
       // 120 
        rule.getTestScheduler().advanceTimeTo(120, TimeUnit.SECONDS);
        verify(mvpView, times(120)).countdownNext(anyString());
       // 
        verify(mvpView).countdownComplete();
    }

    @Test
    public void testLogin() throws Exception {

        initRxJava();

        mPresenter.login("123", "123");
        verify(mvpView).showToast(" ");

        mPresenter.login("13000000000", "123");
        verify(mvpView).showToast(" ");

        mPresenter.login("13000000000", "123456");

        verify(mvpView).showProgress();

        verify(mvpView).loginSuccess();

        verify(mvpView).closeProgress();

    }

    private void initRxJava() {
        RxJavaPlugins.reset();
        RxJavaPlugins.setIoSchedulerHandler(new Function<Scheduler, Scheduler>() {
            @Override
            public Scheduler apply(Scheduler scheduler) throws Exception {
                return Schedulers.trampoline();
            }
        });
        RxAndroidPlugins.reset();
        RxAndroidPlugins.setMainThreadSchedulerHandler(new Function<Scheduler, Scheduler>() {
            @Override
            public Scheduler apply(Scheduler scheduler) throws Exception {
                return Schedulers.trampoline();
            }
        });
    }
}
 

This article is not much content, mainly an integration of the previous explanations, the next article will talk about the unit testing of MVP combined with Dagger. All codes have been uploaded to Github . Hope you guys will give me a lot of praise and support!