[Android]Android MVP&依赖注入&单元测试
以下内容为原创,欢迎转载,转载请注明
来自天天博客:http://www.cnblogs.com/tiantianbyconan/p/5422443.html
Android MVP&依赖注入&单元测试
注意:为了区分
MVP
中的View
与Android
中控件的View
,以下MVP
中的View
使用Viewer
来表示。
这里暂时先只讨论 Viewer
和 Presenter
,Model
暂时不去涉及。
1.1 MVP 基础框架
1.1.1 前提
首先需要解决以下问题:
MVP
中把Layout布局和Activity
等组件作为Viewer
层,增加了Presenter
,Presenter
层与Model
层进行业务的交互,完成后再与Viewer
层交互,进行回调来刷新UI。这样一来,业务逻辑的工作都交给了Presenter
中进行,使得Viewer
层与Model
层的耦合度降低,Viewer
中的工作也进行了简化。但是在实际项目中,随着逻辑的复杂度越来越大,Viewer
(如Activity
)臃肿的缺点仍然体现出来了,因为Activity
中还是充满了大量与Viewer
层无关的代码,比如各种事件的处理派发,就如MVC
中的那样Viewer
层和Controller
代码耦合在一起无法自拔。
转自我之前的博客(http://www.cnblogs.com/tiantianbyconan/p/5036289.html)中第二阶段所引发的问题。
解决的方法之一在上述文章中也有提到 —— 加入Controller
层来分担Viewer
的职责。
1.1.2 Contract
根据以上的解决方案,首先考虑到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
。
1.1.3 Viewer层
1.1.3.1 Viewer 抽象
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
的绑定。
1.1.3.2 Viewer 委托实现
又因为,在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 简单生命周期的组件
1.1.3.3 真实 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
方法进行生命周期的绑定。
1.1.4 Controller 层
1.1.4.1 Controller 抽象
前面讲过,Controller
是需要关心Viewer
生命周期的,所以需要实现OnViewerLifecycleListener
接口。
public interface Controller extends OnViewerLifecycleListener {
void bind(Viewer bindViewer);
}
又提供一个bind()
方法来进行对自身进行绑定到对应的Viewer
上面。
1.1.4.2 Controller 实现
调用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点击事件的回调。
1.1.5 Presenter 层
1.1.5.1 Presenter 抽象
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();
}
1.1.5.2 Presenter RxJava 抽象
因为项目技术需求,需要实现对RxJava
的支持,因此,这里对Presenter
进行相关的扩展,提供两个方法以便于Presenter
对任务的扩展。
public interface RxPresenter extends Presenter {
void goSubscription(Subscription subscription);
void removeSubscription(Subscription subscription);
}
goSubscription()
方法主要用处是,订阅时缓存该订阅对象到Presenter
中,便于管理(怎么管理,下面会讲到)。
removeSubscription()
方法可以从Presenter
中管理的订阅缓存中移除掉该订阅。
1.1.5.3 Presenter RxJava 实现
在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
对象。
1.1.5.4 Presenter 具体实现
在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() {
}
})
);
}
// ...
}
1.1.6 Model 层
暂不讨论。
1.2 针对 MVP 进行依赖注入
上面提到,Viewer
、Controller
和Presenter
中都使用了RInject
注解来进行依赖的注入。
这里并没有使用其他第三方实现的DI
框架,比如Dagger/Dagger2
等,而是自己实现的Rapier,它的原理与Dagger2
类似,会在编译时期生成一些扩展扩展类来简化代码,比如前面的BuyingRequestPostSucceedView_Rapier
、BuyingRequestPostSucceedPresenter_Rapier
、BuyingRequestPostSucceedController_Rapier
等。它也支持Named
、Lazy
等功能,但是它比Dagger2
更加轻量,Module
的使用方式更加简单,更加倾向于对Module
的复用,更强的可控性,但是由于这次的重构主要是基于在兼容旧版本的情况下使用,暂时没有加上Scope
的支持。
之后再针对这个Rapier
库进行详细讨论。
1.3 针对 MVP 进行单元测试
这里主要还是讨论针对Viewer
和Presenter
的单元测试。
1.3.1 针对 Viewer 进行单元测试
针对Viewer
进行单元测试,这里不涉及任何业务相关的逻辑,而且,Viewer
层的测试都是UI相关,必须要Android环境,所以需要在手机或者模拟器安装一个test
apk,然后进行测试。
为了不被Viewer
中的Controller
和Presenter
的逻辑所干扰,我们必须要mock掉Viewer
中的Controller
和Presenter
对象,又因为Controller
对象是通过依赖注入的方式提供的,也就是来自Rapier
中的Module
,所以,我们只需要mock掉Viewer
对应的module
。
1.3.1.1 如果 Viewer 是 View
如果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中。
1.3.1.2 如果 Viewer 是 Activity
如果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
成功了。
1.3.2 针对 Presenter 进行单元测试
1.3.2.1 测试与 Android SDK 分离
Presenter
的单元测试与 Viewer
不一样,在Presenter
中不应该有Android SDK
相关存在,所有的Inteactor/Api
等都是与Android
解耦的。显然更加不能有TextView
等存在。正是因为这个,使得它可以基于PC上的JVM来进行单元测试,也就是说,Presenter
测试不需要Android环境,省去了安装到手机或者模拟器的步骤。
怎么去避免Anroid
相关的SDK在Presenter
中存在?
的确有极个别的SDK很难避免,比如Log
。
1.3.2.1.1 使用 XLog 与 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()
1.3.2.2 异步操作同步化
因为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());
}
}
[Android]Android MVP&依赖注入&单元测试的更多相关文章
- Android——初探Dagger2依赖注入
1,在做项目时,经常需要在一个对象里去创建另一个对象的示例,这种行为是产生耦合的常见形式,对于一个大型项目来说,过多的相互依赖会导致代码难以维护,很容易就会碰到修改一个小需求需要大面积的修改各种代码, ...
- 用Dagger2在Android中实现依赖注入
依赖注入这个模式(模式已经用烂了,这里再烂一次)是用来给应用的各部分解耦的.使应用开发更加可扩展,更容易维护.通过本文你会学到如何使用Dagger2来处理依赖. 简介 如果以对象需要另外的一个对象才能 ...
- 浅析android中的依赖注入
这几年针对Android推出了不少View注入框架,例如ButterKnife.我们首先来了解一下使用这些框架有什么好处,其实好处很明显:它可以减少大量的findViewById以及setOnClic ...
- spring依赖注入单元测试:expected single matching bean but found 2
异常信息:org.springframework.beans.factory.UnsatisfiedDependencyException: Caused by: org.springframewor ...
- [Android]使用Dagger 2依赖注入 - DI介绍(翻译)
以下内容为原创,欢迎转载,转载请注明 来自天天博客:http://www.cnblogs.com/tiantianbyconan/p/5092083.html 使用Dagger 2依赖注入 - DI介 ...
- [Android]使用Dagger 2依赖注入 - API(翻译)
以下内容为原创,欢迎转载,转载请注明 来自天天博客:http://www.cnblogs.com/tiantianbyconan/p/5092525.html 使用Dagger 2依赖注入 - API ...
- [Android]使用Dagger 2依赖注入 - 自定义Scope(翻译)
以下内容为原创,欢迎转载,转载请注明 来自天天博客:http://www.cnblogs.com/tiantianbyconan/p/5095426.html 使用Dagger 2依赖注入 - 自定义 ...
- 浅析Dagger2依赖注入实现过程
Dragger2是Android应用开发中一个非常优秀的依赖注入框架.本文主要通过结合Google给出的MVP开发案例todo-mvp-dagger(GitHub连接地址:https://github ...
- Andriod中的依赖注入
Web后端开发者应该对依赖注入都比较熟悉,至于Android又是如何进行依赖注入的呢?在这篇文章中,让我们一起通过一个例子了解一下在Android中进行依赖注入的好处. AndroidAnnotati ...
随机推荐
- 转-基于NodeJS的14款Web框架
基于NodeJS的14款Web框架 2014-10-16 23:28 作者: NodeJSNet 来源: 本站 浏览: 1,399 次阅读 我要评论暂无评论 字号: 大 中 小 摘要: 在几年的时间里 ...
- 内网穿透神器ngrok
相信做Web开发的同学们,经常会遇到需要将本地部署的Web应用能够让公网环境直接访问到的情况,例如微信应用调试.支付宝接口调试等.这个时候,一个叫ngrok的神器可能会帮到你,它提供了一个能够在公网安 ...
- 拉格朗日插值法——用Python进行数值计算
插值法的伟大作用我就不说了.... 那么贴代码? 首先说一下下面几点: 1. 已有的数据样本被称之为 "插值节点" 2. 对于特定插值节点,它所对应的插值函数是必定存在且唯一的(关 ...
- ★Kali信息收集~★6.Dmitry:汇总收集
概述: DMitry(Deepmagic Information Gathering Tool)是一个一体化的信息收集工具.它可以用来收集以下信息: 1. 端口扫描 2. whois主机IP和域名信息 ...
- 前端学HTTP之报文起始行
前面的话 如果说HTTP是因特网的信使,那么HTTP报文就是它用来搬东西的包裹了.HTTP报文是在HTTP应用程序之间发送的简单的格式化数据块,每条报文都包含一条来自客户端的请求,或者一条来自服务器的 ...
- ES6之module
该博客原文地址:http://www.cnblogs.com/giggle/p/5572118.html 一.module概述 JavaScript一直没有模块体系,但是伴随着ES6的到来,modul ...
- XML技术之SAX解析器
1.解析XML文件有三种解析方法:DOM SAX DOM4J. 2.首先SAX解析技术只能读取XML文档中的数据信息,不能对其文档中的数据进行添加,删除,修改操作:这就是SAX解析技术的一个缺陷. 3 ...
- c 算牌器代码
int main() { // 算牌器 ]; ; do { printf("请输入牌名: \n"); scanf("%2s",char_name); ; ]) ...
- SharePoint 2103 Check user permission on list
一.需求: check user 对SharePoint list 的permission 代码如下: private static string GetListPermission(SPList l ...
- 【基于WinForm+Access局域网共享数据库的项目总结】之篇三:Access远程连接数据库和窗体打包部署
篇一:WinForm开发总体概述与技术实现 篇二:WinForm开发扇形图统计和Excel数据导出 篇三:Access远程连接数据库和窗体打包部署 [小记]:最近基于WinForm+Access数据库 ...