MVP+单元测试探索

作者: 小鱼爱记录 | 来源:发表于2016-08-03 21:11 被阅读572次

    一、单元测试的好处?


    1. 提升我们面向对象设计的能力!
      因为有时候,我们不得不注意对象和方法划分的颗粒度,以保证核心业务逻辑的可测试性!
    2. 让我们的代码更稳健!
      因为我们可以用测试来覆盖尽可能多的场景。这是真机调试很难做到的。
    3. 让开发时的调试更快!
      因为很多时候都不必在手机运行app即可测试了。
    4. 让后期的重构更可靠!
      没有测试作为保障的重构都是耍流氓!

    二、什么是MVP模式?


    所谓MVP(Model-View-Presenter)模式。是将APP的结构分为三层:

    view - 界面逻辑层


    1. 提供UI交互
    2. 在presenter的控制下修改UI。
    3. 将业务事件交由presenter处理。

    注意. View层不存储数据,不与Model层交互。

    presenter - 业务逻辑层


    1. 对UI的各种业务事件进行相应处理。也许是与Model层交互,也许自己进行一些计算,也许控制后台Task,Service
    2. 对各种订阅事件进行响应,修改UI。
    3. 临时存储页面相关数据。

    注意. Presenter内不出现View引用。
    这里我的用法是有View的引用的,不过是抽象引用,没有直接依赖Activity,不然就没法测试Presenter了。

    model - 数据模型层


    1. 从网络,数据库,文件,传感器,第三方等数据源读写数据。
    2. 对外部的数据类型进行解析转换为APP内部数据交由上层处理。
    3. 对数据的临时存储,管理,协调上层数据请求。

    如图示,里面的activity,presenter,model均为例子:



    这样将复杂的功能分割为各层内的小问题。各层内功能单一。易于功能修改拓展与Debug。解耦的设计,独立的模块,更有利于分工开发与测试。

    三、基本用法


    我下面的用法中用到了RxJava+Retrofit,
    不熟悉RxJava的,可以先看我的博文:Retrofit基本用法和流程分析,
    不熟悉Retrofit的,可以先看我的博文:自定义RxJava之旅
    引用了以下测试库:

    testCompile 'junit:junit:4.12'
    testCompile 'org.mockito:mockito-all:2.0.2-beta'

    下面以我们常见的登录场景为例:
    首先定义了一个契约类:

    /**
     * 契约类
     */
    public class LoginContract {
        public interface View {
            void showToast(String message);
        }
    
        public interface Presenter {
            Observable<LoginResponse> login(String username, String password);
        }
    }
    

    在后面还会使用这个验证工具类

    /**
     * 验证工具类
     */
    public class ValidateUtil {
    
        //检查,第一个是表达式,第二个是提示信息
        public static void check(boolean expression, String msg) throws ValidateException {
            if (expression) throw new ValidateException(msg);
        }
    
        //自定义异常
        public static class ValidateException extends Exception {
            public ValidateException(String message) {
                super(message);
            }
        }
    }
    

    1.界面逻辑层


    用Activity或Fragment来作为View层

    这个里面是没法用JUnit测试的,因为里面调用了Android的API,需要有模拟器支持,之后准备引用Robolectric来测试androidTest,这个框架自己实现了一套Android的API,可以在java环境调用Android的一些api。

    /**
     * 界面逻辑
     */
    public class LoginActivity extends MyBaseActivity implements LoginContract.View {
    
        @Bind(R.id.et_password)
        EditText mEtPassword;
        @Bind(R.id.et_username)
        EditText mEtUsername;
        private LoginPresenter mPresenter;
        private Subscription mSubscription;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_login);
            ButterKnife.bind(this);
            mPresenter = new LoginPresenter(this, new UserModel());
        }
    
        @Override
        public void onDestroy() {
            super.onDestroy();
            if (mSubscription != null) {
                mSubscription.unsubscribe();
            }
        }
    
        @OnClick(R.id.tv_login)
        public void onClick() {
            String username = mEtUsername.getText().toString();
            String password = mEtPassword.getText().toString();
            Observable<LoginResponse> observable = mPresenter.login(username, password);
            if (observable == null) return;
            mSubscription = observable
                    .compose(new WebTransformer<>(this))
                    .subscribe(new WebSubscriber<LoginResponse>() {
                        @Override
                        public void onSuccess(LoginResponse response) {
                            parseResponse(response);
                        }
                    });
        }
    
        @Override
        public void showToast(String message) {
            ToastHelper.show(message);
        }
    
        public void parseResponse(LoginResponse response) {
            SkipManager.gotoOrder(getActivity());
        }
    }
    

    2.业务逻辑层


    这个是纯粹的Java类,可以用JUnit测试。

    这也是MVP架构的好处,可以很方便我们测试Presenter层的业务逻辑!

    /**
     * 业务逻辑
     */
    public class LoginPresenter implements LoginContract.Presenter {
        private LoginContract.View mView;
        private UserModel mUserModel;
    
        public LoginPresenter(LoginContract.View view, UserModel userModel) {
            mView = view;
            mUserModel = userModel;
        }
    
        @Override
        public Observable<LoginResponse> login(String username, String password) {
            try {
                ValidateUtil.check(username.isEmpty(), "用户名没有填写");
                ValidateUtil.check(password.isEmpty(), "密码没有填写");
                return mUserModel.performLogin(username, password);
            } catch (ValidateUtil.ValidateException e) {
                mView.showToast(e.getMessage());
            }
            return null;
        }
    
    }
    

    3.数据模型层


    这个是数据模型层,主要是和网络、数据库、缓存等打交道。

    也是没法用JUnit测试的。这个后期也准备用Robolectric来测试看看。

    /**
     * 用户模型
     */
    public class UserModel {
        public Observable<LoginResponse> performLogin(String username, String password) {
            return WebHelper.getWebInterface()
                    .login(new LoginRequest(username, password))
                    .map(new Func1<LoginResponse, LoginResponse>() {
                        @Override
                        public LoginResponse call(LoginResponse response) {
                            response.setPassword(password);
                            CacheManager.saveLoginInfo(response);
                            return response;
                        }
                    });
        }
    }
    

    4.使用JUnit+Mockito来测试Presenter


    这里面涉及到JUnit和Mockito的一些简单用法,不熟悉的朋友可以去看看我下面的参考目录

    /**
     * 测试登录
     */
    public class LoginPresenterTest {
    
        private LoginPresenter mPresenter;
        private LoginContract.View mView;
        private UserModel mModel;
    
        private String mUsername;
        private String mPassword;
    
        @Before
        public void setUp() {
            mModel = Mockito.mock(UserModel.class);
            mView = Mockito.mock(LoginContract.View.class);
            mPresenter = new LoginPresenter(mView, mModel);
        }
    
        //测试没有用户名的场景
        @Test
        public void testLoginNoUserName() {
            mUsername = "";
            mPassword = "12345";
            mPresenter.login(mUsername, mPassword);
            Mockito.verify(mView).showToast("用户名没有填写");
        }
    
        //测试没有密码的场景
        @Test
        public void testLoginNoPassword() {
            mUsername = "yutianran";
            mPassword = "";
            mPresenter.login(mUsername, mPassword);
            Mockito.verify(mView).showToast("密码没有填写");
        }
    
        //测试用户名和密码都有的场景
        @Test
        public void testLogin() {
            mUsername = "yutianran";
            mPassword = "12345";
            mPresenter.login(mUsername, mPassword);
            Mockito.verify(mModel).performLogin(mUsername, mPassword);
        }
    }
    

    run一下这个测试类,输出:


    导出为Html即为:


    当然,我们可以直接run整个test包,运行所有的测试类的,导出则为:


    我们还可以使用gradle来运行所有测试类的

    ./gradlew testDebugUnitTest

    编译成功后,可在这个目录下查看生成的Html文件


    参考目录:


    1. Android单元测试: 首先,从是什么开始
    2. Google原味mvp实践
    3. Android应用中MVP最佳实践

    相关文章

      网友评论

      本文标题:MVP+单元测试探索

      本文链接:https://www.haomeiwen.com/subject/pkxcsttx.html