以下內容為原創,歡迎轉載,轉載請註明 來自天天博客: Android MVP&依賴註入&單元測試 註意 :為了區分 中的 與`Android View MVP View Viewer`來表示。 這裡暫時先只討論 和 ,`Model`暫時不去涉及。 1.1 MVP 基礎框架 1.1.1 前提 首先需要 ...
以下內容為原創,歡迎轉載,轉載請註明
來自天天博客: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
測試範例:
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());
}
}