如下内容为原创,欢迎转载,转载请注明
来自每天博客:http://www.cnblogs.com/tiantianbyconan/p/5422443.html
html
注意:为了区分
MVP
中的View
与Android
中控件的View
,如下MVP
中的View
使用Viewer
来表示。java
这里暂时先只讨论 Viewer
和 Presenter
,Model
暂时不去涉及。android
首先须要解决如下问题:git
MVP
中把Layout布局和Activity
等组件做为Viewer
层,增长了Presenter
,Presenter
层与Model
层进行业务的交互,完成后再与Viewer
层交互,进行回调来刷新UI。这样一来,业务逻辑的工做都交给了Presenter
中进行,使得Viewer
层与Model
层的耦合度下降,Viewer
中的工做也进行了简化。可是在实际项目中,随着逻辑的复杂度愈来愈大,Viewer
(如Activity
)臃肿的缺点仍然体现出来了,由于Activity
中仍是充满了大量与Viewer
层无关的代码,好比各类事件的处理派发,就如MVC
中的那样Viewer
层和Controller
代码耦合在一块儿没法自拔。github
转自我以前的博客(http://www.cnblogs.com/tiantianbyconan/p/5036289.html)中第二阶段所引起的问题。数据库
解决的方法之一在上述文章中也有提到 —— 加入Controller
层来分担Viewer
的职责。api
根据以上的解决方案,首先考虑到Viewer
直接交互的对象多是Presenter
(原来的方式),也有多是Controller
。缓存
若是直接交互的对象是Presenter
,因为Presenter
中可能会进行不少同步、异步操做来调用Model
层的代码,而且会回调到UI来进行UI的更新,因此,咱们须要在Viewer
层对象销毁时可以中止Presenter
中执行的任务,或者执行完成后拦截UI的相关回调。所以,Presenter
中应该绑定Viewer
对象的生命周期(至少Viewer
销毁的生命周期是须要关心的)网络
若是直接交互的对象是Controller
,因为Controller
中会承担Viewer
中的事件回调并派发的职责(好比,ListView item 的点击回调和点击以后对相应的逻辑进行派发、或者Viewer
生命周期方法回调后的处理),因此Controller
层也是须要绑定Viewer
对象的生命周期的。框架
这里,使用Viewer
生命周期回调进行抽象:
public interface OnViewerDestroyListener { void onViewerDestroy(); } public interface OnViewerLifecycleListener extends OnViewerDestroyListener { void onViewerResume(); void onViewerPause(); }
OnViewerDestroyListener
接口提供给须要关心Viewer
层销毁时期的组件,如上,应该是Presenter
所须要关心的。
OnViewerLifecycleListener
接口提供给须要关心Viewer
层生命周期回调的组件,能够根据项目需求增长更多的生命周期的方法,这里咱们只关心Viewer
的resume
和pause
。
Viewer
层,也就是表现层,固然有相关经常使用的UI操做,好比显示一个toast
、显示/取消一个加载进度条等等。除此以外,因为Viewer
层可能会直接与Presenter
或者Controller
层交互,因此应该还提供对这二者的绑定操做,因此以下:
public interface Viewer { Viewer bind(OnViewerLifecycleListener onViewerLifecycleListener); Viewer bind(OnViewerDestroyListener onViewerDestroyListener); Context context(); void showToast(String message); void showToast(int resStringId); void showLoadingDialog(String message); void showLoadingDialog(int resStringId); void cancelLoadingDialog(); }
如上代码,两个bind()
方法就是用于跟Presenter
/Controller
的绑定。
又由于,在Android中Viewer
层对象多是Activity
、Fragment
、View
(包括ViewGroup
),甚至还有本身实现的组件,固然实现的方式通常不外乎上面这几种。因此咱们须要使用统一的Activity
、Fragment
、View
,每一个都须要实现Viewer
接口。为了复用相关代码,这里提供默认的委托实现ViewerDelegate
:
public class ViewerDelegate implements Viewer, OnViewerLifecycleListener { private Context mContext; public ViewerDelegate(Context context) { mContext = context; } private List<OnViewerDestroyListener> mOnViewerDestroyListeners; private List<OnViewerLifecycleListener> mOnViewerLifecycleListeners; private Toast toast; private ProgressDialog loadingDialog; @Override public Viewer bind(OnViewerLifecycleListener onViewerLifecycleListener) { if (null == mOnViewerLifecycleListeners) { mOnViewerLifecycleListeners = new ArrayList<>(); mOnViewerLifecycleListeners.add(onViewerLifecycleListener); } else { if (!mOnViewerLifecycleListeners.contains(onViewerLifecycleListener)) { mOnViewerLifecycleListeners.add(onViewerLifecycleListener); } } return this; } @Override public Viewer bind(OnViewerDestroyListener onViewerDestroyListener) { if (null == mOnViewerDestroyListeners) { mOnViewerDestroyListeners = new ArrayList<>(); mOnViewerDestroyListeners.add(onViewerDestroyListener); } else { if (!mOnViewerDestroyListeners.contains(onViewerDestroyListener)) { mOnViewerDestroyListeners.add(onViewerDestroyListener); } } return this; } @Override public Context context() { return mContext; } @Override public void showToast(String message) { if (!checkViewer()) { return; } if (null == toast) { toast = Toast.makeText(mContext, "", Toast.LENGTH_SHORT); toast.setGravity(Gravity.CENTER, 0, 0); } toast.setText(message); toast.show(); } @Override public void showToast(int resStringId) { if (!checkViewer()) { return; } showToast(mContext.getString(resStringId)); } @Override public void showLoadingDialog(String message) { if (!checkViewer()) { return; } if (null == loadingDialog) { loadingDialog = new ProgressDialog(mContext); loadingDialog.setCanceledOnTouchOutside(false); } loadingDialog.setMessage(message); loadingDialog.show(); } @Override public void showLoadingDialog(int resStringId) { if (!checkViewer()) { return; } showLoadingDialog(mContext.getString(resStringId)); } @Override public void cancelLoadingDialog() { if (!checkViewer()) { return; } if (null != loadingDialog) { loadingDialog.cancel(); } } public boolean checkViewer() { return null != mContext; } @Override public void onViewerResume() { if (null != mOnViewerLifecycleListeners) { for (OnViewerLifecycleListener oll : mOnViewerLifecycleListeners) { oll.onViewerResume(); } } } @Override public void onViewerPause() { if (null != mOnViewerLifecycleListeners) { for (OnViewerLifecycleListener oll : mOnViewerLifecycleListeners) { oll.onViewerPause(); } } } @Override public void onViewerDestroy() { if (null != mOnViewerLifecycleListeners) { for (OnViewerLifecycleListener oll : mOnViewerLifecycleListeners) { oll.onViewerDestroy(); } } if (null != mOnViewerDestroyListeners) { for (OnViewerDestroyListener odl : mOnViewerDestroyListeners) { odl.onViewerDestroy(); } } mContext = null; mOnViewerDestroyListeners = null; mOnViewerLifecycleListeners = null; } }
如上代码:
它提供了默认基本的toast
、和显示/隐藏加载进度条的方法。
它实现了两个重载bind()
方法,并把须要回调的OnViewerLifecycleListener
和OnViewerDestroyListener
对应保存在mOnViewerDestroyListeners
和mOnViewerLifecycleListeners
中。
它实现了OnViewerLifecycleListener
接口,在回调方法中回调到每一个mOnViewerDestroyListeners
和mOnViewerLifecycleListeners
。
mOnViewerDestroyListeners
:Viewer destroy 时的回调,通常状况下只会有Presenter一个对象,可是因为一个Viewer是能够有多个Presenter的,因此可能会维护一个Presenter列表,还有多是其余须要关心 Viewer destroy 的组件
mOnViewerLifecycleListeners
:Viewer 简单的生命周期监听对象,通常状况下只有一个Controller一个对象,可是一个Viewer并不限制只有一个Controller对象,因此可能会维护一个Controller列表,还有多是其余关心 Viewer 简单生命周期的组件
而后在真实的Viewer
中(这里以Activity
为例,其余Fragment
/View
等也是同样),首先,应该实现Viewer
接口,而且应该维护一个委托对象mViewerDelegate
,在实现的Viewer
方法中使用mViewerDelegate
的具体实现。
public class BaseActivity extends AppCompatActivity implements Viewer{ private ViewerDelegate mViewerDelegate; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // ... mViewerDelegate = new ViewerDelegate(this); } @Override protected void onResume() { mViewerDelegate.onViewerResume(); super.onResume(); } @Override protected void onPause() { mViewerDelegate.onViewerPause(); super.onPause(); } @Override protected void onDestroy() { mViewerDelegate.onViewerDestroy(); super.onDestroy(); } @Override public Viewer bind(OnViewerDestroyListener onViewerDestroyListener) { mViewerDelegate.bind(onViewerDestroyListener); return this; } @Override public Viewer bind(OnViewerLifecycleListener onViewerLifecycleListener) { mViewerDelegate.bind(onViewerLifecycleListener); return this; } @Override public Context context() { return mViewerDelegate.context(); } @Override public void showToast(String message) { mViewerDelegate.showToast(message); } @Override public void showToast(int resStringId) { mViewerDelegate.showToast(resStringId); } @Override public void showLoadingDialog(String message) { mViewerDelegate.showLoadingDialog(message); } @Override public void showLoadingDialog(int resStringId) { mViewerDelegate.showLoadingDialog(resStringId); } @Override public void cancelLoadingDialog() { mViewerDelegate.cancelLoadingDialog(); } }
如上,BaseActivity
构建完成。
在具体真实的Viewer
实现中,包含的方法应该都是相似onXxxYyyZzz()
的回调方法,而且这些回调方法应该只进行UI操做,好比onLoadMessage(List<Message> message)
方法在加载完Message
数据后回调该方法来进行UI的更新。
在项目中使用时,应该使用依赖注入来把Controller
对象注入到Viewer
中(这个后面会提到)。
@RInject IBuyingRequestPostSucceedController controller; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // ... BuyingRequestPostSucceedView_Rapier .create() .inject(module, this); controller.bind(this); }
使用RInject
经过BuyingRequestPostSucceedView_Rapier
扩展类来进行注入Controller
对象,而后调用Controller
的bind
方法进行生命周期的绑定。
前面讲过,Controller
是须要关心Viewer
生命周期的,因此须要实现OnViewerLifecycleListener
接口。
public interface Controller extends OnViewerLifecycleListener { void bind(Viewer bindViewer); }
又提供一个bind()
方法来进行对自身进行绑定到对应的Viewer
上面。
调用Viewer
层的bind()
方法来进行绑定,对生命周期进行空实现。
public class BaseController implements Controller { public void bind(Viewer bindViewer) { bindViewer.bind(this); } @Override public void onViewerResume() { // empty } @Override public void onViewerPause() { // empty } @Override public void onViewerDestroy() { // empty } }
该bind()
方法除了用于绑定Viewer
以外,还可让子类重写用于作为Controller的初始化方法,可是注意重写的时候必需要调用super.bind()
。
具体Controller
实现中,应该只包含相似onXxxYyyZzz()
的回调方法,而且这些回调方法应该都是各类事件回调,好比onClick()
用于View点击事件的回调,onItemClick()
表示AdapterView item点击事件的回调。
Presenter
层,做为沟通View
和Model
的桥梁,它从Model
层检索数据后,返回给View
层,它也能够决定与View
层的交互操做。
前面讲到过,View
也是与Presenter
直接交互的,Presenter中可能会进行不少同步、异步操做来调用Model层的代码,而且会回调到UI来进行UI的更新,因此,咱们须要在Viewer层对象销毁时可以中止Presenter中执行的任务,或者执行完成后拦截UI的相关回调。
所以:
Presenter
中应该也有bind()
方法来进行与Viewer
层的生命周期的绑定Presenter
中应该提供一个方法closeAllTask()
来终止或拦截掉UI相关的异步任务。以下:
public interface Presenter extends OnViewerDestroyListener { void bind(Viewer bindViewer); void closeAllTask(); }
由于项目技术需求,须要实现对RxJava
的支持,所以,这里对Presenter
进行相关的扩展,提供两个方法以便于Presenter
对任务的扩展。
public interface RxPresenter extends Presenter { void goSubscription(Subscription subscription); void removeSubscription(Subscription subscription); }
goSubscription()
方法主要用处是,订阅时缓存该订阅对象到Presenter
中,便于管理(怎么管理,下面会讲到)。
removeSubscription()
方法能够从Presenter
中管理的订阅缓存中移除掉该订阅。
在Presenter RxJava 实现(RxBasePresenter
)中,咱们使用WeakHashMap
来构建一个弱引用的Set
,用它来缓存全部订阅。在调用goSubscription()
方法中,把对应的Subscription
加入到Set
中,在removeSubscription()
方法中,把对应的Subscription
从Set
中移除掉。
public class RxBasePresenter implements RxPresenter { private static final String TAG = RxBasePresenter.class.getSimpleName(); private final Set<Subscription> subscriptions = Collections.newSetFromMap(new WeakHashMap<Subscription, Boolean>()); @Override public void closeAllTask() { synchronized (subscriptions) { Iterator iter = this.subscriptions.iterator(); while (iter.hasNext()) { Subscription subscription = (Subscription) iter.next(); XLog.i(TAG, "closeAllTask[subscriptions]: " + subscription); if (null != subscription && !subscription.isUnsubscribed()) { subscription.unsubscribe(); } iter.remove(); } } } @Override public void goSubscription(Subscription subscription) { synchronized (subscriptions) { this.subscriptions.add(subscription); } } @Override public void removeSubscription(Subscription subscription) { synchronized (subscriptions) { XLog.i(TAG, "removeSubscription: " + subscription); if (null != subscription && !subscription.isUnsubscribed()) { subscription.unsubscribe(); } this.subscriptions.remove(subscription); } } @Override public void bind(Viewer bindViewer) { bindViewer.bind(this); } @Override public void onViewerDestroy() { closeAllTask(); } }
如上代码,在onViewerDestroy()
回调时(由于跟Viewer
生命周期进行了绑定),会调用closeAllTask
把全部缓存中的Subscription
取消订阅。
注意:由于缓存中使用了弱引用,因此上面的
removeSubscription
不须要再去手动调用,在订阅completed后,gc天然会回收掉没有强引用指向的Subscription
对象。
在Presenter
具体的实现中,一样依赖注入各类来自Model
层的Interactor/Api
(网络、数据库、文件等等),而后订阅这些对象返回的Observable
,而后进行订阅,并调用goSubscription()
缓存Subscription
:
public class BuyingRequestPostSucceedPresenter extends RxBasePresenter implements IBuyingRequestPostSucceedPresenter { private IBuyingRequestPostSucceedView viewer; @RInject ApiSearcher apiSearcher; public BuyingRequestPostSucceedPresenter(IBuyingRequestPostSucceedView viewer, BuyingRequestPostSucceedPresenterModule module) { this.viewer = viewer; // inject BuyingRequestPostSucceedPresenter_Rapier .create() .inject(module, this); } @Override public void loadSomeThing(final String foo, final String bar) { goSubscription( apiSearcher.searcherSomeThing(foo, bar) .compose(TransformerBridge.<OceanServerResponse<SomeThing>>subscribeOnNet()) .map(new Func1<OceanServerResponse<SomeThing>, SomeThing>() { @Override public SomeThing call(OceanServerResponse<SomeThing> response) { return response.getBody(); } }) .compose(TransformerBridge.<SomeThing>observableOnMain()) .subscribe(new Subscriber<SomeThing>() { @Override public void onError(Throwable e) { XLog.e(TAG, "", e); } @Override public void onNext(SomeThing someThing) { XLog.d(TAG, "XLog onNext..."); viewer.onLoadSomeThing(someThing); } @Override public void onCompleted() { } }) ); } // ... }
暂不讨论。
上面提到,Viewer
、Controller
和Presenter
中都使用了RInject
注解来进行依赖的注入。
这里并无使用其余第三方实现的DI
框架,好比Dagger/Dagger2
等,而是本身实现的Rapier,它的原理与Dagger2
相似,会在编译时期生成一些扩展扩展类来简化代码,好比前面的BuyingRequestPostSucceedView_Rapier
、BuyingRequestPostSucceedPresenter_Rapier
、BuyingRequestPostSucceedController_Rapier
等。它也支持Named
、Lazy
等功能,可是它比Dagger2
更加轻量,Module
的使用方式更加简单,更加倾向于对Module
的复用,更强的可控性,可是因为此次的重构主要是基于在兼容旧版本的状况下使用,暂时没有加上Scope
的支持。
以后再针对这个Rapier
库进行详细讨论。
这里主要仍是讨论针对Viewer
和Presenter
的单元测试。
针对Viewer
进行单元测试,这里不涉及任何业务相关的逻辑,并且,Viewer
层的测试都是UI相关,必需要Android环境,因此须要在手机或者模拟器安装一个test
apk,而后进行测试。
为了避免被Viewer
中的Controller
和Presenter
的逻辑所干扰,咱们必需要mock掉Viewer
中的Controller
和Presenter
对象,又由于Controller
对象是经过依赖注入的方式提供的,也就是来自Rapier
中的Module
,因此,咱们只须要mock掉Viewer
对应的module
。
若是Viewer
层是由View
实现的,好比继承FrameLayout
。这个时候,测试时,就必需要放在一个Activity
中测试(Fragment
也同样,也必须依赖于Activity
),因此咱们应该有一个专门用于测试View/Fragment
的Activity
—— TestContainerActivity
,以下:
public class TestContainerActivity extends BaseActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } }
记得在AndroidManifest.xml
中注册。
前面说过,咱们须要mock掉Module
。
若是Viewer
是View
,mock掉Module
就很是容易了,只要在View
中提供一个传入mock的Module
的构造方法便可,以下:
@VisibleForTesting public BuyingRequestPostSucceedView(Context context, BuyingRequestPostSucceedModule module) { super(context); // inject BuyingRequestPostSucceedView_Rapier .create() .inject(module, this); }
如上代码,这里为测试专门提供了一个构造方法来进行对Module
的mock,以后的测试以下:
BuyingRequestPostSucceedView requestPostSucceedView; @Rule public ActivityTestRule<TestContainerActivity> mActivityTestRule = new ActivityTestRule<TestContainerActivity>(TestContainerActivity.class) { @Override protected void afterActivityLaunched() { super.afterActivityLaunched(); final TestContainerActivity activity = getActivity(); logger("afterActivityLaunched"); activity.runOnUiThread(new Runnable() { @Override public void run() { BuyingRequestPostSucceedModule module = mock(BuyingRequestPostSucceedModule.class); when(module.pickController()).thenReturn(mock(IBuyingRequestPostSucceedController.class)); requestPostSucceedView = new BuyingRequestPostSucceedView(activity, module); activity.setContentView(requestPostSucceedView); } }); } }; @Test public void testOnLoadSomeThings() { final SomeThings products = mock(SomeThings.class); ArrayList<SomeThing> list = mock(ArrayList.class); SomeThing product = mock(SomeThing.class); when(list.get(anyInt())).thenReturn(product); products.productList = list; TestContainerActivity activity = mActivityTestRule.getActivity(); when(list.size()).thenReturn(1); when(list.isEmpty()).thenReturn(false); activity.runOnUiThread(new Runnable() { @Override public void run() { requestPostSucceedView.onLoadSomeThing(products); } }); onView(withId(R.id.id_tips_you_may_also_like_tv)).check(matches(isDisplayed())); // ... }
如上代码,在TestContainerActivity
启动后,构造一个mock了Module
的待测试View
,并增长到Activity
的content view中。
若是Viewer
是Activity
,因为它原本就是Activity,因此它不须要借助TestContainerActivity
来测试;mock module
时就不能使用构造方法的方式了,由于咱们是不能直接对Activity
进行实例化的,那应该怎么办呢?
通常状况下,咱们会在调用onCreate
方法的时候去进行对依赖的注入,也就是调用XxxYyyZzz_Rapier
扩展类,并且,若是这个Activity
须要在一启动就去进行一些数据请求,咱们要拦截掉这个请求,由于这个请求返回的数据可能会对咱们的UI测试形成干扰,因此咱们须要在onCreate
在被调用以前把module
mock掉。
首先看test support 中的 ActivityTestRule
这个类,它提供了如下几个方法:
getActivityIntent()
:这个方法只能在Intent中增长携带的参数,咱们要mock的是整个Module
,没法序列化,因此也没法经过这个传入。
beforeActivityLaunched()
:这个方法回调时,Activity
实例尚未生成,因此没法拿到Activity
实例,并进行Module
的替换。
afterActivityFinished()
:这个方法就更不可能了-.-
afterActivityLaunched()
:这个方法看它的源码(无关代码已省略):
public T launchActivity(@Nullable Intent startIntent) { // ... beforeActivityLaunched(); // The following cast is correct because the activity we're creating is of the same type as // the one passed in mActivity = mActivityClass.cast(mInstrumentation.startActivitySync(startIntent)); mInstrumentation.waitForIdleSync(); afterActivityLaunched(); return mActivity; }
如上代码,afterActivityLaunched()
方法是在真正启动Activity
(mInstrumentation.startActivitySync(startIntent)
)后调用的。可是显然这个方法是同步的,以后再进入源码,来查看启动的流程,整个流程有些复杂我就不赘述了,能够查看我之前写的分析启动流程的博客(http://www.cnblogs.com/tiantianbyconan/p/5017056.html),最后会调用mInstrumentation.callActivityOnCreate(...)
。
可是由于测试时,启动Activity
的过程也是同步的,因此显然这个方法是在onCreate()
被调用后才会被回调的,因此,这个方法也不行。
既然貌似已经找到了mock的正确位置,那就继续分析下去:
这里的mInstrumentation
是哪一个Instrumentation
实例呢?
咱们回到ActivityTestRule
中:
public ActivityTestRule(Class<T> activityClass, boolean initialTouchMode, boolean launchActivity) { mActivityClass = activityClass; mInitialTouchMode = initialTouchMode; mLaunchActivity = launchActivity; mInstrumentation = InstrumentationRegistry.getInstrumentation(); }
继续进入InstrumentationRegistry.getInstrumentation()
:
public static Instrumentation getInstrumentation() { Instrumentation instance = sInstrumentationRef.get(); if (null == instance) { throw new IllegalStateException("No instrumentation registered! " + "Must run under a registering instrumentation."); } return instance; }
继续查找sInstrumentationRef
是在哪里set
进去的:
public static void registerInstance(Instrumentation instrumentation, Bundle arguments) { sInstrumentationRef.set(instrumentation); sArguments.set(new Bundle(arguments)); }
继续查找调用,终于在MonitoringInstrumentation
中找到:
@Override public void onCreate(Bundle arguments) { // ... InstrumentationRegistry.registerInstance(this, arguments); // ... }
因此,测试使用的MonitoringInstrumentation
,而后进入MonitoringInstrumentation
的callActivityOnCreate()
方法:
@Override public void callActivityOnCreate(Activity activity, Bundle bundle) { mLifecycleMonitor.signalLifecycleChange(Stage.PRE_ON_CREATE, activity); super.callActivityOnCreate(activity, bundle); mLifecycleMonitor.signalLifecycleChange(Stage.CREATED, activity); }
既然咱们须要在Activity
真正执行onCreate()
方法时拦截掉,那如上代码,只要关心signalLifecycleChange()
方法,发现了ActivityLifecycleCallback
的回调:
public void signalLifecycleChange(Stage stage, Activity activity) { // ... Iterator<WeakReference<ActivityLifecycleCallback>> refIter = mCallbacks.iterator(); while (refIter.hasNext()) { ActivityLifecycleCallback callback = refIter.next().get(); if (null == callback) { refIter.remove(); } else { // ... callback.onActivityLifecycleChanged(activity, stage); // ... } }
因此,问题解决了,咱们只要添加一个Activity
生命周期回调就搞定了,代码以下:
ActivityLifecycleMonitorRegistry.getInstance().addLifecycleCallback(new ActivityLifecycleCallback() { @Override public void onActivityLifecycleChanged(Activity activity, Stage stage) { logger("onActivityLifecycleChanged, activity" + activity + ", stage: " + stage); if(activity instanceof SomethingActivity && Stage.PRE_ON_CREATE == stage){ logger("onActivityLifecycleChanged, got it!!!"); ((SomethingActivity)activity).setModule(mock(SomethingModule.class)); } } });
至此,Activity
的 mock module
成功了。
Presenter
的单元测试与 Viewer
不同,在Presenter
中不该该有Android SDK
相关存在,全部的Inteactor/Api
等都是与Android
解耦的。显然更加不能有TextView
等存在。正是由于这个,使得它能够基于PC上的JVM来进行单元测试,也就是说,Presenter
测试不须要Android环境,省去了安装到手机或者模拟器的步骤。
怎么去避免Anroid
相关的SDK在Presenter
中存在?
的确有极个别的SDK很难避免,好比Log
。
因此,咱们须要一个XLog
:
public class XLog { private static IXLog delegate; private static boolean DEBUG = true; public static void setDebug(boolean debug) { XLog.DEBUG = debug; } public static void setDelegate(IXLog delegate) { XLog.delegate = delegate; } public static void v(String tag, String msg) { if (DEBUG && null != delegate) { delegate.v(tag, msg); } } public static void v(String tag, String msg, Throwable tr) { if (DEBUG && null != delegate) { delegate.v(tag, msg, tr); } } public static void d(String tag, String msg) { if (DEBUG && null != delegate) { delegate.d(tag, msg); } } // ...
在Android环境中使用的策略:
XLog.setDelegate(new XLogDef());
其中XLogDef
类中的实现为原生Androd SDK的Log实现。
在测试环境中使用的策略:
logDelegateSpy = Mockito.spy(new XLogJavaTest()); XLog.setDelegate(logDelegateSpy);
其中XLogJavaTest
使用的是纯Java的System.out.println()
由于Presenter
中会有不少的异步任务存在,可是在细粒度的单元测试中,没有异步任务存在的必要性,相应反而增长了测试复杂度。因此,咱们应该把全部异步任务切换成同步操做。
调度的切换使用的是RxJava
,因此全部切换到主线程也是使用了Android SDK
。这里也要采用策略进行处理。
首先定义了几种不一样的ScheduleType
:
public class SchedulerType { public static final int MAIN = 0x3783; public static final int NET = 0x8739; public static final int DB = 0x1385; // ... }
在Schedule
选择器中根据ScheduleType
进行对应类型的实现:
SchedulerSelector schedulerSelector = SchedulerSelector.get(); schedulerSelector.putScheduler(SchedulerType.MAIN, new SchedulerSelector.SchedulerCreation<Scheduler>() { @Override public Scheduler create() { return AndroidSchedulers.mainThread(); } }); schedulerSelector.putScheduler(SchedulerType.NET, new SchedulerSelector.SchedulerCreation<Scheduler>() { @Override public Scheduler create() { return Schedulers.from(THREAD_POOL_EXECUTOR_NETWORK); } }); schedulerSelector.putScheduler(SchedulerType.DB, new SchedulerSelector.SchedulerCreation<Scheduler>() { @Override public Scheduler create() { return Schedulers.from(THREAD_POOL_EXECUTOR_DATABASE); } }); // ...
当测试时,对调度选择器中的不一样类型的实现进行以下替换:
SchedulerSelector.get().putScheduler(SchedulerType.NET, new SchedulerSelector.SchedulerCreation<Scheduler>() { @Override public Scheduler create() { return Schedulers.immediate(); } }); SchedulerSelector.get().putScheduler(SchedulerType.MAIN, new SchedulerSelector.SchedulerCreation<Scheduler>() { @Override public Scheduler create() { return Schedulers.immediate(); } });
把全部调度都改为当前线程执行便可。
最后Presenter
测试几个范例:
@Mock AccountContract.IAccountViewer viewer; @Mock UserInteractor userInteractor; AccountPresenter presenter; @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); presenter = new AccountPresenter(viewer); presenter.userInteractor = userInteractor; } @Test public void requestEditUserInfo() throws Exception { // case 1, succeed reset(viewer); resetLog(); when(userInteractor.requestEditUserInfo(any(User.class))).thenReturn(Observable.just(anyBoolean())); presenter.requestEditUserInfo(new User()); verifyOnce(viewer).onRequestEditUserInfo(); // case 2, null reset(viewer); resetLog(); when(userInteractor.requestEditUserInfo(any(User.class))).thenReturn(Observable.just(null)); presenter.requestEditUserInfo(new User()); verifyOnce(viewer).onRequestEditUserInfo(); // case 3, error assertFailedAndError(() -> userInteractor.requestEditUserInfo(any(User.class)), () -> presenter.requestEditUserInfo(new User())); }
public class SBuyingRequestPostSucceedViewPresenterTest extends BaseJavaTest { @Mock public IBuyingRequestPostSucceedView viewer; @Mock public BuyingRequestPostSucceedPresenterModule module; @Mock public ApiSearcher apiSearcher; public IBuyingRequestPostSucceedPresenter presenter; @Before public void setUp() { MockitoAnnotations.initMocks(this); when(module.pickApiSearcher()).thenReturn(apiSearcher); presenter = new BuyingRequestPostSucceedPresenter(viewer, module); } @Test public void testLoadSomethingSuccess() throws TimeoutException { // Mock success observable when(apiSearcher.searcherSomething(anyString(), anyString(), anyString())) .thenReturn(Observable.create(new Observable.OnSubscribe<OceanServerResponse<Something>>() { @Override public void call(Subscriber<? super OceanServerResponse<Something>> subscriber) { try { OceanServerResponse<Something> oceanServerResponse = mock(OceanServerResponse.class); when(oceanServerResponse.getBody(any(Class.class))).thenReturn(mock(Something.class)); subscriber.onNext(oceanServerResponse); subscriber.onCompleted(); } catch (Throwable throwable) { subscriber.onError(throwable); } }I })); final ExecuteStuff executeStuff = new ExecuteStuff(); Answer succeedAnswer = new Answer() { @Override public Object answer(InvocationOnMock invocationOnMock) throws Throwable { loggerMockAnswer(invocationOnMock); executeStuff.setSucceed(true); return null; } }; doAnswer(succeedAnswer).when(viewer).onLoadSomething(Matchers.any(Something.class)); presenter.loadSomething("whatever", "whatever"); logger("loadSomething result: " + executeStuff.isSucceed()); Assert.assertTrue("testLoadSomethingSuccess result true", executeStuff.isSucceed()); } @Test public void testLoadSomethingFailed() throws TimeoutException { // Mock error observable when(apiSearcher.searcherRFQInterestedProductsSuggestion(anyString(), anyString(), anyString())) .thenReturn(Observable.<OceanServerResponse<Something>>error(new RuntimeException("mock error observable"))); final ExecuteStuff executeStuff = new ExecuteStuff(); Answer failedAnswer = new Answer() { @Override public Object answer(InvocationOnMock invocationOnMock) throws Throwable { loggerMockAnswer(invocationOnMock); executeStuff.setSucceed(false); return null; } }; doAnswerWhenLogError(failedAnswer); presenter.loadSomething("whatever", "whatever"); logger("testLoadSomethingFailed result: " + executeStuff.isSucceed()); Assert.assertFalse("testLoadSomethingFailed result false", executeStuff.isSucceed()); } }