以下內容為原創,歡迎轉載,轉載請注明
來自天天博客: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());
}
}
