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+单元测试探索

    一、单元测试的好处? 提升我们面向对象设计的能力!因为有时候,我们不得不注意对象和方法划分的颗粒度,以保证核心业务...

  • Angular2--属性型指令单元测试

    属性型指令的单元测试 这里依然用文本高亮的指令来说明。 单一的单元测试 但是测试单一的用例无法探索该指令的全部能力...

  • 怎么劝说自己开始写单元测试

    写在前面 此文是一个菜鸡对单元测试的探索,具备参考价值,不具备使用价值。 单元测试的意义 必要性:软件的持续集成、...

  • Vue单元测试探索

    为什么要单元测试? 项目的现状 当前我在公司里负责的项目,可以分为两类: 一类是相似度很高的项目,比如管理后台,这...

  • [iOS单元测试系列]单元测试框架选型

    这段时间在团队里一直在负责对单元测试的探索和落地,感觉有必要总结下自己这段时间的探索成果和大家分享。加之已经忙的很...

  • [iOS单元测试系列]单元测试框架选型

    这段时间在团队里一直在负责对单元测试的探索和落地,感觉有必要总结下自己这段时间的探索成果和大家分享。加之已经忙的很...

  • Android架构选型(MVP+DataBinding)

    通过代码对比,详细讲解MVC,MVP,MVVM之间应该如何选择,以及对Android单元测试的探索。本文的侧重点在...

  • 测者的测试技术手册:智能化测试框架EvoSuite的一个坑以及填

    问题 最近在不断地学习和探索EvoSuite框架的时候,在生产JUnit单元测试框架后,出现如下问题: Excep...

  • Android Clean 架构

    业务逻辑应该清楚地分开并且独立于框架 先看下我们熟悉的MVP架构 再看下我们熟悉的MVP+管理类 提前预览下Cle...

  • 单元测试

    内容 单元测试 参考文章: [iOS单元测试系列]单元测试框架选型 iOS单元测试:Specta + Expect...

网友评论

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

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