天天看点

[Android]Android MVP&依赖注入&单元测试Android MVP&依赖注入&单元测试

以下内容为原创,欢迎转载,转载请注明

注意:为了区分<code>MVP</code>中的<code>View</code>与<code>Android</code>中控件的<code>View</code>,以下<code>MVP</code>中的<code>View</code>使用<code>Viewer</code>来表示。

这里暂时先只讨论 <code>Viewer</code> 和 <code>Presenter</code>,<code>Model</code>暂时不去涉及。

首先需要解决以下问题:

<code>MVP</code>中把Layout布局和<code>Activity</code>等组件作为<code>Viewer</code>层,增加了<code>Presenter</code>,<code>Presenter</code>层与<code>Model</code>层进行业务的交互,完成后再与<code>Viewer</code>层交互,进行回调来刷新UI。这样一来,业务逻辑的工作都交给了<code>Presenter</code>中进行,使得<code>Viewer</code>层与<code>Model</code>层的耦合度降低,<code>Viewer</code>中的工作也进行了简化。但是在实际项目中,随着逻辑的复杂度越来越大,<code>Viewer</code>(如<code>Activity</code>)臃肿的缺点仍然体现出来了,因为<code>Activity</code>中还是充满了大量与<code>Viewer</code>层无关的代码,比如各种事件的处理派发,就如<code>MVC</code>中的那样<code>Viewer</code>层和<code>Controller</code>代码耦合在一起无法自拔。

解决的方法之一在上述文章中也有提到 —— 加入<code>Controller</code>层来分担<code>Viewer</code>的职责。

根据以上的解决方案,首先考虑到<code>Viewer</code>直接交互的对象可能是<code>Presenter</code>(原来的方式),也有可能是<code>Controller</code>。

如果直接交互的对象是<code>Presenter</code>,由于<code>Presenter</code>中可能会进行很多同步、异步操作来调用<code>Model</code>层的代码,并且会回调到UI来进行UI的更新,所以,我们需要在<code>Viewer</code>层对象销毁时能够停止<code>Presenter</code>中执行的任务,或者执行完成后拦截UI的相关回调。因此,<code>Presenter</code>中应该绑定<code>Viewer</code>对象的生命周期(至少<code>Viewer</code>销毁的生命周期是需要关心的)

如果直接交互的对象是<code>Controller</code>,由于<code>Controller</code>中会承担<code>Viewer</code>中的事件回调并派发的职责(比如,ListView item 的点击回调和点击之后对相应的逻辑进行派发、或者<code>Viewer</code>生命周期方法回调后的处理),所以<code>Controller</code>层也是需要绑定<code>Viewer</code>对象的生命周期的。

这里,使用<code>Viewer</code>生命周期回调进行抽象:

<code>OnViewerDestroyListener</code>接口提供给需要关心<code>Viewer</code>层销毁时期的组件,如上,应该是<code>Presenter</code>所需要关心的。

<code>OnViewerLifecycleListener</code>接口提供给需要关心<code>Viewer</code>层生命周期回调的组件,可以根据项目需求增加更多的生命周期的方法,这里我们只关心<code>Viewer</code>的<code>resume</code>和<code>pause</code>。

<code>Viewer</code>层,也就是表现层,当然有相关常用的UI操作,比如显示一个<code>toast</code>、显示/取消一个加载进度条等等。除此之外,由于<code>Viewer</code>层可能会直接与<code>Presenter</code>或者<code>Controller</code>层交互,所以应该还提供对这两者的绑定操作,所以如下:

如上代码,两个<code>bind()</code>方法就是用于跟<code>Presenter</code>/<code>Controller</code>的绑定。

又因为,在Android中<code>Viewer</code>层对象可能是<code>Activity</code>、<code>Fragment</code>、<code>View</code>(包括<code>ViewGroup</code>),甚至还有自己实现的组件,当然实现的方式一般不外乎上面这几种。所以我们需要使用统一的<code>Activity</code>、<code>Fragment</code>、<code>View</code>,每个都需要实现<code>Viewer</code>接口。为了复用相关代码,这里提供默认的委托实现<code>ViewerDelegate</code>:

如上代码:

它提供了默认基本的<code>toast</code>、和显示/隐藏加载进度条的方法。

它实现了两个重载<code>bind()</code>方法,并把需要回调的<code>OnViewerLifecycleListener</code>和<code>OnViewerDestroyListener</code>对应保存在<code>mOnViewerDestroyListeners</code>和<code>mOnViewerLifecycleListeners</code>中。

它实现了<code>OnViewerLifecycleListener</code>接口,在回调方法中回调到每个<code>mOnViewerDestroyListeners</code>和<code>mOnViewerLifecycleListeners</code>。

<code>mOnViewerDestroyListeners</code>:Viewer destroy 时的回调,一般情况下只会有Presenter一个对象,但是由于一个Viewer是可以有多个Presenter的,所以可能会维护一个Presenter列表,还有可能是其他需要关心 Viewer destroy 的组件

<code>mOnViewerLifecycleListeners</code>:Viewer 简单的生命周期监听对象,一般情况下只有一个Controller一个对象,但是一个Viewer并不限制只有一个Controller对象,所以可能会维护一个Controller列表,还有可能是其他关心 Viewer 简单生命周期的组件

然后在真实的<code>Viewer</code>中(这里以<code>Activity</code>为例,其他<code>Fragment</code>/<code>View</code>等也是一样),首先,应该实现<code>Viewer</code>接口,并且应该维护一个委托对象<code>mViewerDelegate</code>,在实现的<code>Viewer</code>方法中使用<code>mViewerDelegate</code>的具体实现。

如上,<code>BaseActivity</code>构建完成。

在具体真实的<code>Viewer</code>实现中,包含的方法应该都是类似<code>onXxxYyyZzz()</code>的回调方法,并且这些回调方法应该只进行UI操作,比如<code>onLoadMessage(List&lt;Message&gt; message)</code>方法在加载完<code>Message</code>数据后回调该方法来进行UI的更新。

在项目中使用时,应该使用依赖注入来把<code>Controller</code>对象注入到<code>Viewer</code>中(这个后面会提到)。

使用<code>RInject</code>通过<code>BuyingRequestPostSucceedView_Rapier</code>扩展类来进行注入<code>Controller</code>对象,然后调用<code>Controller</code>的<code>bind</code>方法进行生命周期的绑定。

前面讲过,<code>Controller</code>是需要关心<code>Viewer</code>生命周期的,所以需要实现<code>OnViewerLifecycleListener</code>接口。

又提供一个<code>bind()</code>方法来进行对自身进行绑定到对应的<code>Viewer</code>上面。

调用<code>Viewer</code>层的<code>bind()</code>方法来进行绑定,对生命周期进行空实现。

该<code>bind()</code>方法除了用于绑定<code>Viewer</code>之外,还可以让子类重写用于做为Controller的初始化方法,但是注意重写的时候必须要调用<code>super.bind()</code>。

具体<code>Controller</code>实现中,应该只包含类似<code>onXxxYyyZzz()</code>的回调方法,并且这些回调方法应该都是各种事件回调,比如<code>onClick()</code>用于View点击事件的回调,<code>onItemClick()</code>表示AdapterView item点击事件的回调。

<code>Presenter</code>层,作为沟通 <code>View</code> 和 <code>Model</code> 的桥梁,它从 <code>Model</code> 层检索数据后,返回给 <code>View</code> 层,它也可以决定与 <code>View</code> 层的交互操作。

前面讲到过,<code>View</code>也是与<code>Presenter</code>直接交互的,Presenter中可能会进行很多同步、异步操作来调用Model层的代码,并且会回调到UI来进行UI的更新,所以,我们需要在Viewer层对象销毁时能够停止Presenter中执行的任务,或者执行完成后拦截UI的相关回调。

因此:

<code>Presenter</code> 中应该也有<code>bind()</code>方法来进行与<code>Viewer</code>层的生命周期的绑定

<code>Presenter</code> 中应该提供一个方法<code>closeAllTask()</code>来终止或拦截掉UI相关的异步任务。

如下:

因为项目技术需求,需要实现对<code>RxJava</code>的支持,因此,这里对<code>Presenter</code>进行相关的扩展,提供两个方法以便于<code>Presenter</code>对任务的扩展。

<code>goSubscription()</code>方法主要用处是,订阅时缓存该订阅对象到<code>Presenter</code>中,便于管理(怎么管理,下面会讲到)。

<code>removeSubscription()</code>方法可以从<code>Presenter</code>中管理的订阅缓存中移除掉该订阅。

在Presenter RxJava 实现(<code>RxBasePresenter</code>)中,我们使用<code>WeakHashMap</code>来构建一个弱引用的<code>Set</code>,用它来缓存所有订阅。在调用<code>goSubscription()</code>方法中,把对应的<code>Subscription</code>加入到<code>Set</code>中,在<code>removeSubscription()</code>方法中,把对应的<code>Subscription</code>从<code>Set</code>中移除掉。

如上代码,在<code>onViewerDestroy()</code>回调时(因为跟<code>Viewer</code>生命周期进行了绑定),会调用<code>closeAllTask</code>把所有缓存中的<code>Subscription</code>取消订阅。

注意:因为缓存中使用了弱引用,所以上面的<code>removeSubscription</code>不需要再去手动调用,在订阅completed后,gc自然会回收掉没有强引用指向的<code>Subscription</code>对象。

在<code>Presenter</code>具体的实现中,同样依赖注入各种来自<code>Model</code>层的<code>Interactor/Api</code>(网络、数据库、文件等等),然后订阅这些对象返回的<code>Observable</code>,然后进行订阅,并调用<code>goSubscription()</code>缓存<code>Subscription</code>:

暂不讨论。

上面提到,<code>Viewer</code>、<code>Controller</code>和<code>Presenter</code>中都使用了<code>RInject</code>注解来进行依赖的注入。

之后再针对这个<code>Rapier</code>库进行详细讨论。

这里主要还是讨论针对<code>Viewer</code>和<code>Presenter</code>的单元测试。

针对<code>Viewer</code>进行单元测试,这里不涉及任何业务相关的逻辑,而且,<code>Viewer</code>层的测试都是UI相关,必须要Android环境,所以需要在手机或者模拟器安装一个<code>test</code> apk,然后进行测试。

为了不被<code>Viewer</code>中的<code>Controller</code>和<code>Presenter</code>的逻辑所干扰,我们必须要mock掉<code>Viewer</code>中的<code>Controller</code>和<code>Presenter</code>对象,又因为<code>Controller</code>对象是通过依赖注入的方式提供的,也就是来自<code>Rapier</code>中的<code>Module</code>,所以,我们只需要mock掉<code>Viewer</code>对应的<code>module</code>。

如果<code>Viewer</code>层是由<code>View</code>实现的,比如继承<code>FrameLayout</code>。这个时候,测试时,就必须要放在一个<code>Activity</code>中测试(<code>Fragment</code>也一样,也必须依赖于<code>Activity</code>),所以我们应该有一个专门用于测试<code>View/Fragment</code>的<code>Activity</code> —— <code>TestContainerActivity</code>,如下:

记得在<code>AndroidManifest.xml</code>中注册。

前面说过,我们需要mock掉<code>Module</code>。

如果<code>Viewer</code>是<code>View</code>,mock掉<code>Module</code>就非常容易了,只要在<code>View</code>中提供一个传入mock的<code>Module</code>的构造方法即可,如下:

如上代码,这里为测试专门提供了一个构造方法来进行对<code>Module</code>的mock,之后的测试如下:

如上代码,在<code>TestContainerActivity</code>启动后,构造一个mock了<code>Module</code>的待测试<code>View</code>,并增加到<code>Activity</code>的content view中。

如果<code>Viewer</code>是<code>Activity</code>,由于它本来就是Activity,所以它不需要借助<code>TestContainerActivity</code>来测试;mock <code>module</code>时就不能使用构造方法的方式了,因为我们是不能直接对<code>Activity</code>进行实例化的,那应该怎么办呢?

一般情况下,我们会在调用<code>onCreate</code>方法的时候去进行对依赖的注入,也就是调用<code>XxxYyyZzz_Rapier</code>扩展类,而且,如果这个<code>Activity</code>需要在一启动就去进行一些数据请求,我们要拦截掉这个请求,因为这个请求返回的数据可能会对我们的UI测试造成干扰,所以我们需要在<code>onCreate</code>在被调用之前把<code>module</code> mock掉。

首先看test support 中的 <code>ActivityTestRule</code>这个类,它提供了以下几个方法:

<code>getActivityIntent()</code>:这个方法只能在Intent中增加携带的参数,我们要mock的是整个<code>Module</code>,无法序列化,所以也无法通过这个传入。

<code>beforeActivityLaunched()</code>:这个方法回调时,<code>Activity</code>实例还没有生成,所以无法拿到<code>Activity</code>实例,并进行<code>Module</code>的替换。

<code>afterActivityFinished()</code>:这个方法就更不可能了-.-

<code>afterActivityLaunched()</code>:这个方法看它的源码(无关代码已省略):

但是因为测试时,启动<code>Activity</code>的过程也是同步的,所以显然这个方法是在<code>onCreate()</code>被调用后才会被回调的,所以,这个方法也不行。

既然貌似已经找到了mock的正确位置,那就继续分析下去:

这里的<code>mInstrumentation</code>是哪个<code>Instrumentation</code>实例呢?

我们回到<code>ActivityTestRule</code>中:

继续进入<code>InstrumentationRegistry.getInstrumentation()</code>:

继续查找<code>sInstrumentationRef</code>是在哪里<code>set</code>进去的:

继续查找调用,终于在<code>MonitoringInstrumentation</code>中找到:

所以,测试使用的<code>MonitoringInstrumentation</code>,然后进入<code>MonitoringInstrumentation</code>的<code>callActivityOnCreate()</code>方法:

既然我们需要在<code>Activity</code>真正执行<code>onCreate()</code>方法时拦截掉,那如上代码,只要关心<code>signalLifecycleChange()</code>方法,发现了<code>ActivityLifecycleCallback</code>的回调:

所以,问题解决了,我们只要添加一个<code>Activity</code>生命周期回调就搞定了,代码如下:

至此,<code>Activity</code>的 mock <code>module</code>成功了。

<code>Presenter</code> 的单元测试与 <code>Viewer</code> 不一样,在<code>Presenter</code>中不应该有<code>Android SDK</code>相关存在,所有的<code>Inteactor/Api</code>等都是与<code>Android</code>解耦的。显然更加不能有<code>TextView</code>等存在。正是因为这个,使得它可以基于PC上的JVM来进行单元测试,也就是说,<code>Presenter</code>测试不需要Android环境,省去了安装到手机或者模拟器的步骤。

怎么去避免<code>Anroid</code>相关的SDK在<code>Presenter</code>中存在?

的确有极个别的SDK很难避免,比如<code>Log</code>。

所以,我们需要一个<code>XLog</code>:

在Android环境中使用的策略:

其中<code>XLogDef</code>类中的实现为原生Androd SDK的Log实现。

在测试环境中使用的策略:

其中<code>XLogJavaTest</code>使用的是纯Java的<code>System.out.println()</code>

因为<code>Presenter</code>中会有很多的异步任务存在,但是在细粒度的单元测试中,没有异步任务存在的必要性,相应反而增加了测试复杂度。所以,我们应该把所有异步任务切换成同步操作。

调度的切换使用的是<code>RxJava</code>,所以所有切换到主线程也是使用了<code>Android SDK</code>。这里也要采用策略进行处理。

首先定义了几种不同的<code>ScheduleType</code>:

在<code>Schedule</code>选择器中根据<code>ScheduleType</code>进行对应类型的实现:

当测试时,对调度选择器中的不同类型的实现进行如下替换:

把所有调度都改成当前线程执行即可。

最后<code>Presenter</code>测试几个范例:

本文转自天天_byconan博客园博客,原文链接:http://www.cnblogs.com/tiantianbyconan/p/5422443.html,如需转载请自行联系原作者