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


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

注意:为了区分MVP中的ViewAndroid中控件的View,以下MVP中的View使用Viewer来表示。

这里暂时先只讨论 ViewerPresenterModel暂时不去涉及。

1.1 MVP 基础框架

1.1.1 前提

首先需要解决以下问题:

MVP中把Layout布局和Activity等组件作为Viewer层,增加了PresenterPresenter层与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生命周期回调进行抽象:

1
2
3
4
5
6
7
8
public interface OnViewerDestroyListener {
void onViewerDestroy();
}

public interface OnViewerLifecycleListener extends OnViewerDestroyListener {
void onViewerResume();
void onViewerPause();
}

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

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

1.1.3 Viewer层

1.1.3.1 Viewer 抽象

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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层对象可能是ActivityFragmentView(包括ViewGroup),甚至还有自己实现的组件,当然实现的方式一般不外乎上面这几种。所以我们需要使用统一的ActivityFragmentView,每个都需要实现Viewer接口。为了复用相关代码,这里提供默认的委托实现ViewerDelegate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
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()方法,并把需要回调的OnViewerLifecycleListenerOnViewerDestroyListener对应保存在mOnViewerDestroyListenersmOnViewerLifecycleListeners中。

  • 它实现了OnViewerLifecycleListener接口,在回调方法中回调到每个mOnViewerDestroyListenersmOnViewerLifecycleListeners

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的具体实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
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中(这个后面会提到)。

1
2
3
4
5
6
7
8
9
10
11
12
13
@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对象,然后调用Controllerbind方法进行生命周期的绑定。

1.1.4 Controller 层

1.1.4.1 Controller 抽象

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

1
2
3
public interface Controller extends OnViewerLifecycleListener {
void bind(Viewer bindViewer);
}

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

1.1.4.2 Controller 实现

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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层,作为沟通 ViewModel 的桥梁,它从 Model 层检索数据后,返回给 View 层,它也可以决定与 View 层的交互操作。

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

因此:

  • Presenter 中应该也有bind()方法来进行与Viewer层的生命周期的绑定
  • Presenter 中应该提供一个方法closeAllTask()来终止或拦截掉UI相关的异步任务。

如下:

1
2
3
4
public interface Presenter extends OnViewerDestroyListener {
void bind(Viewer bindViewer);
void closeAllTask();
}

1.1.5.2 Presenter RxJava 抽象

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

1
2
3
4
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()方法中,把对应的SubscriptionSet中移除掉。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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 进行依赖注入

上面提到,ViewerControllerPresenter中都使用了RInject注解来进行依赖的注入。

这里并没有使用其他第三方实现的DI框架,比如Dagger/Dagger2等,而是自己实现的Rapier,它的原理与Dagger2类似,会在编译时期生成一些扩展扩展类来简化代码,比如前面的BuyingRequestPostSucceedView_RapierBuyingRequestPostSucceedPresenter_RapierBuyingRequestPostSucceedController_Rapier等。它也支持NamedLazy等功能,但是它比Dagger2更加轻量,Module的使用方式更加简单,更加倾向于对Module的复用,更强的可控性,但是由于这次的重构主要是基于在兼容旧版本的情况下使用,暂时没有加上Scope的支持。

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

1.3 针对 MVP 进行单元测试

这里主要还是讨论针对ViewerPresenter的单元测试。

1.3.1 针对 Viewer 进行单元测试

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

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

1.3.1.1 如果 Viewer 是 View

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

1
2
3
4
5
6
public class TestContainerActivity extends BaseActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
}

记得在AndroidManifest.xml中注册。

前面说过,我们需要mock掉Module

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

1
2
3
4
5
6
7
8
@VisibleForTesting
public BuyingRequestPostSucceedView(Context context, BuyingRequestPostSucceedModule module) {
super(context);
// inject
BuyingRequestPostSucceedView_Rapier
.create()
.inject(module, this);
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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

如果ViewerActivity,由于它本来就是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():这个方法看它的源码(无关代码已省略):

1
2
3
4
5
6
7
8
9
10
11
12
13
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()方法是在真正启动ActivitymInstrumentation.startActivitySync(startIntent))后调用的。但是显然这个方法是同步的,之后再进入源码,来查看启动的流程,整个流程有些复杂我就不赘述了,可以查看我以前写的分析启动流程的博客(http://www.cnblogs.com/tiantianbyconan/p/5017056.html),最后会调用mInstrumentation.callActivityOnCreate(...)

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

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

这里的mInstrumentation是哪个Instrumentation实例呢?

我们回到ActivityTestRule中:

1
2
3
4
5
6
7
public ActivityTestRule(Class<T> activityClass, boolean initialTouchMode,
boolean launchActivity) {
mActivityClass = activityClass;
mInitialTouchMode = initialTouchMode;
mLaunchActivity = launchActivity;
mInstrumentation = InstrumentationRegistry.getInstrumentation();
}

继续进入InstrumentationRegistry.getInstrumentation()

1
2
3
4
5
6
7
8
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进去的:

1
2
3
4
public static void registerInstance(Instrumentation instrumentation, Bundle arguments) {
sInstrumentationRef.set(instrumentation);
sArguments.set(new Bundle(arguments));
}

继续查找调用,终于在MonitoringInstrumentation中找到:

1
2
3
4
5
6
@Override
public void onCreate(Bundle arguments) {
// ...
InstrumentationRegistry.registerInstance(this, arguments);
// ...
}

所以,测试使用的MonitoringInstrumentation,然后进入MonitoringInstrumentationcallActivityOnCreate()方法:

1
2
3
4
5
6
@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的回调:

1
2
3
4
5
6
7
8
9
10
11
12
13
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生命周期回调就搞定了,代码如下:

1
2
3
4
5
6
7
8
9
10
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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环境中使用的策略:

1
XLog.setDelegate(new XLogDef());

其中XLogDef类中的实现为原生Androd SDK的Log实现。

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

1
2
logDelegateSpy = Mockito.spy(new XLogJavaTest());
XLog.setDelegate(logDelegateSpy);

其中XLogJavaTest使用的是纯Java的System.out.println()

1.3.2.2 异步操作同步化

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

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

首先定义了几种不同的ScheduleType

1
2
3
4
5
6
public class SchedulerType {
public static final int MAIN = 0x3783;
public static final int NET = 0x8739;
public static final int DB = 0x1385;
// ...
}

Schedule选择器中根据ScheduleType进行对应类型的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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);
}
});
// ...

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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测试几个范例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@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()));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
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());

}

}


来源博客:Wang Jie's Blog's Blog
本文链接:https://blog.wangjiegulu.com/2016/04/22/Android-Android-MVP-依赖注入-单元测试/
版权声明:本博客所有文章除特别声明外,均采用 CC BY 4.0 CN协议 许可协议。转载请注明出处。