android-MVP架构中Presenter的单元测试

作者: 绮怀先生 | 来源:发表于2018-03-23 21:11 被阅读471次

    一,为什么只对Presenter进行单元测试,而不测试Model和View呢?

    原因1:

    mvp中,全部业务逻辑都集中在这个类中,bug的高发区,只要这块测试好了,app稳定性可以大大提高。

    原因2:

    在mvp架构中model层主要进行负责存储、检索、操纵数据(包括网络请求),这些并不涉及业务逻辑的处理,没能想到可以怎么测试,如果读者有什么好建议可以留言给我;而view层主要进行ui操作,与用户进行交互,更加适合进行UI测试。

    二,如何测试Presenter?

    总共分为两个步骤,以welcome功能模块为例(检测是否来自其他平台用户登录)

    步骤1:

    编写契约类,实现mvp

    契约类:

    /**
     * 欢迎模块 处理其他平台登录用户
     */
    public interface WelcomeContract {
    
        interface View extends BaseView {
    
            void handleError(String errorMsg);
    
            void outsideLoginSuccess(LoginBean loginBean);
        }
    
        interface Presenter extends BasePresenter {
    
            boolean handleData(Intent data);
        }
    
        interface Model extends BaseModel {
    
            void outsideLogin(String jsonData, SubscriberAction subscriberAction);
    
            void outsideLoginSuccess(LoginBean loginBean);
        }
    }
    
    

    presenter层:

    public class WelcomePresenter implements WelcomeContract.Presenter {
    
        private static final String TAG = "WelcomePresenter";
        WelcomeContract.View mView;
        WelcomeContract.Model mModel;
    
        public WelcomePresenter(WelcomeContract.View view, WelcomeContract.Model model) {
            this.mView = view;
            this.mModel = model;
        }
    
        @Override
        public boolean handleData(Intent data) {
    
            if (data != null) {
                try {
                    Uri uri = data.getData();
                    if (uri != null) {
                        String json = uri.getQueryParameter("data");
                        JSONObject jsonObject = new JSONObject(json);
                        Logger.t("outsideLogin").e(jsonObject.toString());
                        if (jsonObject != null) {
                            String userId = null;
                            String userType = null;
                            String source = null;
                            try {
                                userId = jsonObject.getString("userId");
                                userType = jsonObject.getString("userType");
                                source = jsonObject.getString("source");
                            } catch (Exception e) {
                                Logger.e(e, TAG);
                            }
    
                            if (userId == null) {
                                String errorMsg = "userId为空";
                                mView.handleError(errorMsg);
                                return true;
                            }
                            if (userType == null) {
                                String errorMsg = "userType为空";
                                mView.handleError(errorMsg);
                                return true;
                            }
                            if (source == null) {
                                String errorMsg = "source为空";
                                mView.handleError(errorMsg);
                                return true;
                            }
                            mView.showProgressDialog();
                            //验证没有问题,请求服务器获取登录数据
                            mModel.outsideLogin(json, new SubscriberAction<LoginBean>(mView, loginBean -> {
                                mView.dismissProgressDialog();
                                if (loginBean == null) {
                                    mView.handleError("返回数据为空");
                                } else {
                                    mModel.outsideLoginSuccess(loginBean);
                                    mView.outsideLoginSuccess(loginBean);
                                }
                            }, throwable -> {
                                throwable.printStackTrace();
                                if (mView.getVActivity() != null && mView.getVActivity().isFinishing()) {
                                    mView.getVActivity().runOnUiThread(() -> {
                                        mView.handleError(throwable.getMessage());
                                        mView.dismissProgressDialog();
                                    });
                                }
                            }));
    
    
                            return true;
                        } else {
                            mView.handleError("解析json出错");
                            return false;
                        }
                    } else {
                        return false;
                    }
                } catch (Exception e) {
                    Logger.e(e, TAG);
                    mView.handleError("解析登录数据出错");
                    return true;
                }
            }
            return false;
        }
    }
    

    model层:

    public class WelcomeModel implements WelcomeContract.Model {
        private StudentService mService = StudentRetrofitClient.INSTANCE().getService();
    
        public WelcomeModel() {
    
        }
    
        @Override
        public void outsideLogin(String jsonData, SubscriberAction subscriberAction) {
            StudentRetrofitClient.INSTANCE().toSubscribe(mService.outsideLogin(jsonData), subscriberAction);
        }
    
        @Override
        public void outsideLoginSuccess(LoginBean loginBean) {
            LoginBiz.saveLoginData(loginBean);
        }
    
    }
    

    view层就不贴了,主要关注点在presenter层,model层代码有助于测试中参数捕抓的理解

    步骤2:

    编写针对presenter的测试类
    功能写完后,验证业务逻辑是否能处理各种数据的输入。

    特别注意:以前完成了功能后就一直等后台接口数据,接口调通了,心里才踏实;而现在,我不需要等后台接口,直接就能验证presenter的业务逻辑写得好不好,能不能处理各种突发意外情况,这是单元测试的一大好处。单元测试给我最大的感受:一个字: ,两个字:踏实 ,具体一点来说:对自己写的代码不会胆战心惊,不会害怕功能上线了惊呼:我擦,这什么情况?我写的时候完全就没想到会有这种情况发生的!写单元测试其实是意识到自己代码具有局限性的的过程,无论对自己,对项目都是大有裨益的。

    测试内容:

    1,验证handleData(Intent data)能否处理空数据
    2,验证handleData(Intent data)能否处理异常数据
    3,验证handleData(Intent data)能否处理正常数据

    WelcomePresenterTest:

    
    /**
     * Android单元测试示例
     * 使用框架简介:
     * junit(纯java代码可用该框架测试),
     * mockito(模拟数据),
     * robolectric(模拟Android运行环境,可以测试Android代码)
     * 纯java部分的可以通过Junit4来进行单元测试,
     * 而对于用到android自身代码的测试不能依靠Junit进行,
     * 对于这种情况解决方案之一就是使用Robolectric
     */
    
    /**
     * 知识点1,runWith:RobolectricTestRunner
     * 表示测试时使用robolectric运行环境,可以测试Android代码,比如:textview.setText()这样的代码
     * 如果测试Presenter中没有涉及Android代码,则不要加,否则拖慢测试速度。
     */
    @RunWith(RobolectricTestRunner.class)
    /**
     * 知识点2,指定manifest文件,格式如下:
     * @Config(manifest = "../app/AndroidManifest.xml")
     *
     */
    @Config(manifest = Config.NONE)
    
    public class WelcomePresenterTest {
        WelcomeContract.Presenter mPresenter;
        /**
         * 知识点3,@mock 注解介绍:
         * 模拟某个类对象
         * 为什么要模拟?
         * 答:因为这是测试环境,view对象的获取很麻烦很困难,并且view并不是我们测试的对象。
         */
        @Mock
        WelcomeContract.View mView;
        @Mock
        WelcomeContract.Model mModel;
        /**
         * 知识点4:参数捕抓器
         * 用于捕抓model层方法中的参数
         */
        ArgumentCaptor<SubscriberAction> captor;
    
        /**
         * 在测试前的数据初始化
         */
        @Before
        public void setUp() {
            //Mockito的初始化
            MockitoAnnotations.initMocks(this);
            /**
             * 知识点5:Presenter的创建
             * 注意:在view层就需要创建model,将之作为presenter的构造方法参数。
             * 对比之前的写法:mPresenter = new WelcomePresenter(this)的写法
             * 这样的写法好处:model可以在测试中模拟,如果model完全隐藏在presenter的
             * 构造方法中,model还需要用参数捕抓出来,比较麻烦。
             */
            mPresenter = new WelcomePresenter(mView, mModel);
            captor = ArgumentCaptor.forClass(SubscriberAction.class);
            /**
             *知识点6: 把将Rxjava接口调用的异步操作变成同步,加快测试速度。
             */
            UnitTestHelper.openRxTools();
    
        }
    
        /**
         * 传递给presenter的参数异常的测试
         *
         * @throws Exception
         */
        @Test
        public void handleDataFail() throws Exception {
            Intent intent = mock(Intent.class);
            Uri uri = mock(Uri.class);
            intent.setData(uri);
            when(uri.getQueryParameter("data"))
                     //模拟数据为空情况
    //                .thenReturn(null)
    
                     //模拟数据缺失情况,少了userId
                    .thenReturn("{\"source\":\"xxxx\",\"userType\":\"xxxx\"}");
            when(intent.getData()).thenReturn(uri);
    
            mPresenter.handleData(intent);
    //        assertFalse(mPresenter.handleData(intent));
            verify(mView).handleError(any(String.class));
        }
    
        /**
         * 传递给presenter的参数正常的测试
         * @throws Exception
         */
        @Test
        public void handleDataSuccess() throws Exception {
            /**
             * 模拟数据
             */
            Intent intent = mock(Intent.class);
            Uri uri = mock(Uri.class);
            when(uri.getQueryParameter("data")).thenReturn("{\"userId\":\"xxxx\",\"source\":\"xxxx\",\"userType\":\"xxxx\"}");
    
            when(intent.getData()).thenReturn(uri);
    
            mPresenter.handleData(intent);
            /**
             * mPresenter.handleData调用后
             * 1,验证(verify)model是否调用了outsideLogin方法,
             * 2,并且捕获outsideLogin方法参数subscriberAction对象
             */
            verify(mModel).outsideLogin(any(String.class), captor.capture());
            /**
             * 疑问:为什么要捕抓subscriberAction对象?
             * 答:因为模拟调用接口成功中需要用到subscriberAction这个订阅者对象。
             *
             */
            UnitTestHelper.mockCallBack(new LoginBean(), captor.getValue());
            /**
             * 接口数据LoginBean成功模拟返回后
             * 验证(verify)Presenter是否调用了model以及view中outsideLoginSuccess方法。
             */
            verify(mModel).outsideLoginSuccess(any(LoginBean.class));
            verify(mView).outsideLoginSuccess(any(LoginBean.class));
        }
    
    }
    

    UnitTestHelper单元测试工具类:

    
    /**
     * 用于:
     *1,模拟model中网络请求返回的数据
     *2,把RXJava的异步变成同步,方便测试
     */
    public class UnitTestHelper {
    
        public static void mockFailCallBack(SubscriberAction sub) {
            mockCallBack(99,"我错了",null,sub);
        }
        public static void mockFailCallBack(int resultCode,String msg,SubscriberAction sub) {
            mockCallBack(resultCode,msg,null,sub);
        }
        public static void mockEmptyCallBack(SubscriberAction sub) {
            mockCallBack(0,"模拟接口调用成功",null,sub);
        }
        public static void mockCallBack(Object data,SubscriberAction sub) {
            mockCallBack(0,"模拟接口调用成功",data,sub);
        }
        public static void mockCallBack(int resultCode,String msg,Object data,SubscriberAction sub) {
            BaseRetrofitClient.toSubscribe(Observable.just(new HttpResult<>(resultCode,msg,data)),sub);
        }
        private static boolean isInitRxTools = false;
    
        /**
         * 把RXJava的异步变成同步,方便测试
         */
        public static void openRxTools() {
            if (isInitRxTools) {
                return;
            }
            isInitRxTools = true;
    
            RxAndroidSchedulersHook rxAndroidSchedulersHook = new RxAndroidSchedulersHook() {
                @Override
                public Scheduler getMainThreadScheduler() {
                    return Schedulers.immediate();
                }
            };
    
            RxJavaSchedulersHook rxJavaSchedulersHook = new RxJavaSchedulersHook() {
                @Override
                public Scheduler getIOScheduler() {
                    return Schedulers.immediate();
                }
            };
    
            // reset()不是必要,实践中发现不写reset(),偶尔会出错,所以写上保险
            RxAndroidPlugins.getInstance().reset();
            RxAndroidPlugins.getInstance().registerSchedulersHook(rxAndroidSchedulersHook);
            RxJavaPlugins.getInstance().reset();
            RxJavaPlugins.getInstance().registerSchedulersHook(rxJavaSchedulersHook);
        }
    }
    

    这两个类是这篇博客的精华所在,耗费了我们Android组不少时间,不少精力探索出来的,有兴趣的读者可以慢慢读这段代码,收获会超乎想象。

    三,Android测试填坑

    1,选框架的坑

    非常建议采用robolectric框架,工欲善其事必先利其器,一开始没有选择robolectric框架,就开始撸单元测试,摔得脸好疼,郁闷了一整天:明明我这样写单元测试没有错的呀,怎么就死活都没法通过测试呢?
    原因在于mvp中测试presenter过程中无可避免会调用Android系统API,而junit不支持,mock也不可能面面俱到,有些方法中Android API藏得比较深,很难都mock到,而用了robolectric框架就完全没有问题。

    robolectric原理:实现一套JVM能运行的Android代码,然后在unit test运行的时候去截取android相关的代码调用,然后转到他们的他们实现的Shadow代码去执行这个调用

    1,创建单元测试类的小坑

    有个同事不知道AS能自动生成测试类,然后说,单元测试好麻烦,创建一个类要写这么多东西。
    贴上一个自动创建测试类的小教程:

    自动创建测试类-步骤1.png 自动创建测试类-步骤2.png 自动创建测试类-步骤3.png

    相关文章

      网友评论

      • 绮怀先生:不是vim,只是android studio的代码配色而已,需要的话,我发给你
      • 丨灬花为伊人醉:我想知道你的vim怎么配的?那个调色真的好看

      本文标题:android-MVP架构中Presenter的单元测试

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