如下內容爲原創,歡迎轉載,轉載請註明
來自每天博客:http://www.cnblogs.com/tiantianbyconan/p/5422443.html
html
注意:爲了區分
MVP
中的View
與Android
中控件的View
,如下MVP
中的View
使用Viewer
來表示。java
這裏暫時先只討論 Viewer
和 Presenter
,Model
暫時不去涉及。android
首先須要解決如下問題:git
MVP
中把Layout佈局和Activity
等組件做爲Viewer
層,增長了Presenter
,Presenter
層與Model
層進行業務的交互,完成後再與Viewer
層交互,進行回調來刷新UI。這樣一來,業務邏輯的工做都交給了Presenter
中進行,使得Viewer
層與Model
層的耦合度下降,Viewer
中的工做也進行了簡化。可是在實際項目中,隨着邏輯的複雜度愈來愈大,Viewer
(如Activity
)臃腫的缺點仍然體現出來了,由於Activity
中仍是充滿了大量與Viewer
層無關的代碼,好比各類事件的處理派發,就如MVC
中的那樣Viewer
層和Controller
代碼耦合在一塊兒沒法自拔。github
轉自我以前的博客(http://www.cnblogs.com/tiantianbyconan/p/5036289.html)中第二階段所引起的問題。數據庫
解決的方法之一在上述文章中也有提到 —— 加入Controller
層來分擔Viewer
的職責。api
根據以上的解決方案,首先考慮到Viewer
直接交互的對象多是Presenter
(原來的方式),也有多是Controller
。緩存
若是直接交互的對象是Presenter
,因爲Presenter
中可能會進行不少同步、異步操做來調用Model
層的代碼,而且會回調到UI來進行UI的更新,因此,咱們須要在Viewer
層對象銷燬時可以中止Presenter
中執行的任務,或者執行完成後攔截UI的相關回調。所以,Presenter
中應該綁定Viewer
對象的生命週期(至少Viewer
銷燬的生命週期是須要關心的)網絡
若是直接交互的對象是Controller
,因爲Controller
中會承擔Viewer
中的事件回調並派發的職責(好比,ListView item 的點擊回調和點擊以後對相應的邏輯進行派發、或者Viewer
生命週期方法回調後的處理),因此Controller
層也是須要綁定Viewer
對象的生命週期的。框架
這裏,使用Viewer
生命週期回調進行抽象:
public interface OnViewerDestroyListener { void onViewerDestroy(); } public interface OnViewerLifecycleListener extends OnViewerDestroyListener { void onViewerResume(); void onViewerPause(); }
OnViewerDestroyListener
接口提供給須要關心Viewer
層銷燬時期的組件,如上,應該是Presenter
所須要關心的。
OnViewerLifecycleListener
接口提供給須要關心Viewer
層生命週期回調的組件,能夠根據項目需求增長更多的生命週期的方法,這裏咱們只關心Viewer
的resume
和pause
。
Viewer
層,也就是表現層,固然有相關經常使用的UI操做,好比顯示一個toast
、顯示/取消一個加載進度條等等。除此以外,因爲Viewer
層可能會直接與Presenter
或者Controller
層交互,因此應該還提供對這二者的綁定操做,因此以下:
public interface Viewer { Viewer bind(OnViewerLifecycleListener onViewerLifecycleListener); Viewer bind(OnViewerDestroyListener onViewerDestroyListener); Context context(); void showToast(String message); void showToast(int resStringId); void showLoadingDialog(String message); void showLoadingDialog(int resStringId); void cancelLoadingDialog(); }
如上代碼,兩個bind()
方法就是用於跟Presenter
/Controller
的綁定。
又由於,在Android中Viewer
層對象多是Activity
、Fragment
、View
(包括ViewGroup
),甚至還有本身實現的組件,固然實現的方式通常不外乎上面這幾種。因此咱們須要使用統一的Activity
、Fragment
、View
,每一個都須要實現Viewer
接口。爲了複用相關代碼,這裏提供默認的委託實現ViewerDelegate
:
public class ViewerDelegate implements Viewer, OnViewerLifecycleListener { private Context mContext; public ViewerDelegate(Context context) { mContext = context; } private List<OnViewerDestroyListener> mOnViewerDestroyListeners; private List<OnViewerLifecycleListener> mOnViewerLifecycleListeners; private Toast toast; private ProgressDialog loadingDialog; @Override public Viewer bind(OnViewerLifecycleListener onViewerLifecycleListener) { if (null == mOnViewerLifecycleListeners) { mOnViewerLifecycleListeners = new ArrayList<>(); mOnViewerLifecycleListeners.add(onViewerLifecycleListener); } else { if (!mOnViewerLifecycleListeners.contains(onViewerLifecycleListener)) { mOnViewerLifecycleListeners.add(onViewerLifecycleListener); } } return this; } @Override public Viewer bind(OnViewerDestroyListener onViewerDestroyListener) { if (null == mOnViewerDestroyListeners) { mOnViewerDestroyListeners = new ArrayList<>(); mOnViewerDestroyListeners.add(onViewerDestroyListener); } else { if (!mOnViewerDestroyListeners.contains(onViewerDestroyListener)) { mOnViewerDestroyListeners.add(onViewerDestroyListener); } } return this; } @Override public Context context() { return mContext; } @Override public void showToast(String message) { if (!checkViewer()) { return; } if (null == toast) { toast = Toast.makeText(mContext, "", Toast.LENGTH_SHORT); toast.setGravity(Gravity.CENTER, 0, 0); } toast.setText(message); toast.show(); } @Override public void showToast(int resStringId) { if (!checkViewer()) { return; } showToast(mContext.getString(resStringId)); } @Override public void showLoadingDialog(String message) { if (!checkViewer()) { return; } if (null == loadingDialog) { loadingDialog = new ProgressDialog(mContext); loadingDialog.setCanceledOnTouchOutside(false); } loadingDialog.setMessage(message); loadingDialog.show(); } @Override public void showLoadingDialog(int resStringId) { if (!checkViewer()) { return; } showLoadingDialog(mContext.getString(resStringId)); } @Override public void cancelLoadingDialog() { if (!checkViewer()) { return; } if (null != loadingDialog) { loadingDialog.cancel(); } } public boolean checkViewer() { return null != mContext; } @Override public void onViewerResume() { if (null != mOnViewerLifecycleListeners) { for (OnViewerLifecycleListener oll : mOnViewerLifecycleListeners) { oll.onViewerResume(); } } } @Override public void onViewerPause() { if (null != mOnViewerLifecycleListeners) { for (OnViewerLifecycleListener oll : mOnViewerLifecycleListeners) { oll.onViewerPause(); } } } @Override public void onViewerDestroy() { if (null != mOnViewerLifecycleListeners) { for (OnViewerLifecycleListener oll : mOnViewerLifecycleListeners) { oll.onViewerDestroy(); } } if (null != mOnViewerDestroyListeners) { for (OnViewerDestroyListener odl : mOnViewerDestroyListeners) { odl.onViewerDestroy(); } } mContext = null; mOnViewerDestroyListeners = null; mOnViewerLifecycleListeners = null; } }
如上代碼:
它提供了默認基本的toast
、和顯示/隱藏加載進度條的方法。
它實現了兩個重載bind()
方法,並把須要回調的OnViewerLifecycleListener
和OnViewerDestroyListener
對應保存在mOnViewerDestroyListeners
和mOnViewerLifecycleListeners
中。
它實現了OnViewerLifecycleListener
接口,在回調方法中回調到每一個mOnViewerDestroyListeners
和mOnViewerLifecycleListeners
。
mOnViewerDestroyListeners
:Viewer destroy 時的回調,通常狀況下只會有Presenter一個對象,可是因爲一個Viewer是能夠有多個Presenter的,因此可能會維護一個Presenter列表,還有多是其餘須要關心 Viewer destroy 的組件
mOnViewerLifecycleListeners
:Viewer 簡單的生命週期監聽對象,通常狀況下只有一個Controller一個對象,可是一個Viewer並不限制只有一個Controller對象,因此可能會維護一個Controller列表,還有多是其餘關心 Viewer 簡單生命週期的組件
而後在真實的Viewer
中(這裏以Activity
爲例,其餘Fragment
/View
等也是同樣),首先,應該實現Viewer
接口,而且應該維護一個委託對象mViewerDelegate
,在實現的Viewer
方法中使用mViewerDelegate
的具體實現。
public class BaseActivity extends AppCompatActivity implements Viewer{ private ViewerDelegate mViewerDelegate; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // ... mViewerDelegate = new ViewerDelegate(this); } @Override protected void onResume() { mViewerDelegate.onViewerResume(); super.onResume(); } @Override protected void onPause() { mViewerDelegate.onViewerPause(); super.onPause(); } @Override protected void onDestroy() { mViewerDelegate.onViewerDestroy(); super.onDestroy(); } @Override public Viewer bind(OnViewerDestroyListener onViewerDestroyListener) { mViewerDelegate.bind(onViewerDestroyListener); return this; } @Override public Viewer bind(OnViewerLifecycleListener onViewerLifecycleListener) { mViewerDelegate.bind(onViewerLifecycleListener); return this; } @Override public Context context() { return mViewerDelegate.context(); } @Override public void showToast(String message) { mViewerDelegate.showToast(message); } @Override public void showToast(int resStringId) { mViewerDelegate.showToast(resStringId); } @Override public void showLoadingDialog(String message) { mViewerDelegate.showLoadingDialog(message); } @Override public void showLoadingDialog(int resStringId) { mViewerDelegate.showLoadingDialog(resStringId); } @Override public void cancelLoadingDialog() { mViewerDelegate.cancelLoadingDialog(); } }
如上,BaseActivity
構建完成。
在具體真實的Viewer
實現中,包含的方法應該都是相似onXxxYyyZzz()
的回調方法,而且這些回調方法應該只進行UI操做,好比onLoadMessage(List<Message> message)
方法在加載完Message
數據後回調該方法來進行UI的更新。
在項目中使用時,應該使用依賴注入來把Controller
對象注入到Viewer
中(這個後面會提到)。
@RInject IBuyingRequestPostSucceedController controller; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // ... BuyingRequestPostSucceedView_Rapier .create() .inject(module, this); controller.bind(this); }
使用RInject
經過BuyingRequestPostSucceedView_Rapier
擴展類來進行注入Controller
對象,而後調用Controller
的bind
方法進行生命週期的綁定。
前面講過,Controller
是須要關心Viewer
生命週期的,因此須要實現OnViewerLifecycleListener
接口。
public interface Controller extends OnViewerLifecycleListener { void bind(Viewer bindViewer); }
又提供一個bind()
方法來進行對自身進行綁定到對應的Viewer
上面。
調用Viewer
層的bind()
方法來進行綁定,對生命週期進行空實現。
public class BaseController implements Controller { public void bind(Viewer bindViewer) { bindViewer.bind(this); } @Override public void onViewerResume() { // empty } @Override public void onViewerPause() { // empty } @Override public void onViewerDestroy() { // empty } }
該bind()
方法除了用於綁定Viewer
以外,還可讓子類重寫用於作爲Controller的初始化方法,可是注意重寫的時候必需要調用super.bind()
。
具體Controller
實現中,應該只包含相似onXxxYyyZzz()
的回調方法,而且這些回調方法應該都是各類事件回調,好比onClick()
用於View點擊事件的回調,onItemClick()
表示AdapterView item點擊事件的回調。
Presenter
層,做爲溝通View
和Model
的橋樑,它從Model
層檢索數據後,返回給View
層,它也能夠決定與View
層的交互操做。
前面講到過,View
也是與Presenter
直接交互的,Presenter中可能會進行不少同步、異步操做來調用Model層的代碼,而且會回調到UI來進行UI的更新,因此,咱們須要在Viewer層對象銷燬時可以中止Presenter中執行的任務,或者執行完成後攔截UI的相關回調。
所以:
Presenter
中應該也有bind()
方法來進行與Viewer
層的生命週期的綁定Presenter
中應該提供一個方法closeAllTask()
來終止或攔截掉UI相關的異步任務。以下:
public interface Presenter extends OnViewerDestroyListener { void bind(Viewer bindViewer); void closeAllTask(); }
由於項目技術需求,須要實現對RxJava
的支持,所以,這裏對Presenter
進行相關的擴展,提供兩個方法以便於Presenter
對任務的擴展。
public interface RxPresenter extends Presenter { void goSubscription(Subscription subscription); void removeSubscription(Subscription subscription); }
goSubscription()
方法主要用處是,訂閱時緩存該訂閱對象到Presenter
中,便於管理(怎麼管理,下面會講到)。
removeSubscription()
方法能夠從Presenter
中管理的訂閱緩存中移除掉該訂閱。
在Presenter RxJava 實現(RxBasePresenter
)中,咱們使用WeakHashMap
來構建一個弱引用的Set
,用它來緩存全部訂閱。在調用goSubscription()
方法中,把對應的Subscription
加入到Set
中,在removeSubscription()
方法中,把對應的Subscription
從Set
中移除掉。
public class RxBasePresenter implements RxPresenter { private static final String TAG = RxBasePresenter.class.getSimpleName(); private final Set<Subscription> subscriptions = Collections.newSetFromMap(new WeakHashMap<Subscription, Boolean>()); @Override public void closeAllTask() { synchronized (subscriptions) { Iterator iter = this.subscriptions.iterator(); while (iter.hasNext()) { Subscription subscription = (Subscription) iter.next(); XLog.i(TAG, "closeAllTask[subscriptions]: " + subscription); if (null != subscription && !subscription.isUnsubscribed()) { subscription.unsubscribe(); } iter.remove(); } } } @Override public void goSubscription(Subscription subscription) { synchronized (subscriptions) { this.subscriptions.add(subscription); } } @Override public void removeSubscription(Subscription subscription) { synchronized (subscriptions) { XLog.i(TAG, "removeSubscription: " + subscription); if (null != subscription && !subscription.isUnsubscribed()) { subscription.unsubscribe(); } this.subscriptions.remove(subscription); } } @Override public void bind(Viewer bindViewer) { bindViewer.bind(this); } @Override public void onViewerDestroy() { closeAllTask(); } }
如上代碼,在onViewerDestroy()
回調時(由於跟Viewer
生命週期進行了綁定),會調用closeAllTask
把全部緩存中的Subscription
取消訂閱。
注意:由於緩存中使用了弱引用,因此上面的
removeSubscription
不須要再去手動調用,在訂閱completed後,gc天然會回收掉沒有強引用指向的Subscription
對象。
在Presenter
具體的實現中,一樣依賴注入各類來自Model
層的Interactor/Api
(網絡、數據庫、文件等等),而後訂閱這些對象返回的Observable
,而後進行訂閱,並調用goSubscription()
緩存Subscription
:
public class BuyingRequestPostSucceedPresenter extends RxBasePresenter implements IBuyingRequestPostSucceedPresenter { private IBuyingRequestPostSucceedView viewer; @RInject ApiSearcher apiSearcher; public BuyingRequestPostSucceedPresenter(IBuyingRequestPostSucceedView viewer, BuyingRequestPostSucceedPresenterModule module) { this.viewer = viewer; // inject BuyingRequestPostSucceedPresenter_Rapier .create() .inject(module, this); } @Override public void loadSomeThing(final String foo, final String bar) { goSubscription( apiSearcher.searcherSomeThing(foo, bar) .compose(TransformerBridge.<OceanServerResponse<SomeThing>>subscribeOnNet()) .map(new Func1<OceanServerResponse<SomeThing>, SomeThing>() { @Override public SomeThing call(OceanServerResponse<SomeThing> response) { return response.getBody(); } }) .compose(TransformerBridge.<SomeThing>observableOnMain()) .subscribe(new Subscriber<SomeThing>() { @Override public void onError(Throwable e) { XLog.e(TAG, "", e); } @Override public void onNext(SomeThing someThing) { XLog.d(TAG, "XLog onNext..."); viewer.onLoadSomeThing(someThing); } @Override public void onCompleted() { } }) ); } // ... }
暫不討論。
上面提到,Viewer
、Controller
和Presenter
中都使用了RInject
註解來進行依賴的注入。
這裏並無使用其餘第三方實現的DI
框架,好比Dagger/Dagger2
等,而是本身實現的Rapier,它的原理與Dagger2
相似,會在編譯時期生成一些擴展擴展類來簡化代碼,好比前面的BuyingRequestPostSucceedView_Rapier
、BuyingRequestPostSucceedPresenter_Rapier
、BuyingRequestPostSucceedController_Rapier
等。它也支持Named
、Lazy
等功能,可是它比Dagger2
更加輕量,Module
的使用方式更加簡單,更加傾向於對Module
的複用,更強的可控性,可是因爲此次的重構主要是基於在兼容舊版本的狀況下使用,暫時沒有加上Scope
的支持。
以後再針對這個Rapier
庫進行詳細討論。
這裏主要仍是討論針對Viewer
和Presenter
的單元測試。
針對Viewer
進行單元測試,這裏不涉及任何業務相關的邏輯,並且,Viewer
層的測試都是UI相關,必需要Android環境,因此須要在手機或者模擬器安裝一個test
apk,而後進行測試。
爲了避免被Viewer
中的Controller
和Presenter
的邏輯所幹擾,咱們必需要mock掉Viewer
中的Controller
和Presenter
對象,又由於Controller
對象是經過依賴注入的方式提供的,也就是來自Rapier
中的Module
,因此,咱們只須要mock掉Viewer
對應的module
。
若是Viewer
層是由View
實現的,好比繼承FrameLayout
。這個時候,測試時,就必需要放在一個Activity
中測試(Fragment
也同樣,也必須依賴於Activity
),因此咱們應該有一個專門用於測試View/Fragment
的Activity
—— TestContainerActivity
,以下:
public class TestContainerActivity extends BaseActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } }
記得在AndroidManifest.xml
中註冊。
前面說過,咱們須要mock掉Module
。
若是Viewer
是View
,mock掉Module
就很是容易了,只要在View
中提供一個傳入mock的Module
的構造方法便可,以下:
@VisibleForTesting public BuyingRequestPostSucceedView(Context context, BuyingRequestPostSucceedModule module) { super(context); // inject BuyingRequestPostSucceedView_Rapier .create() .inject(module, this); }
如上代碼,這裏爲測試專門提供了一個構造方法來進行對Module
的mock,以後的測試以下:
BuyingRequestPostSucceedView requestPostSucceedView; @Rule public ActivityTestRule<TestContainerActivity> mActivityTestRule = new ActivityTestRule<TestContainerActivity>(TestContainerActivity.class) { @Override protected void afterActivityLaunched() { super.afterActivityLaunched(); final TestContainerActivity activity = getActivity(); logger("afterActivityLaunched"); activity.runOnUiThread(new Runnable() { @Override public void run() { BuyingRequestPostSucceedModule module = mock(BuyingRequestPostSucceedModule.class); when(module.pickController()).thenReturn(mock(IBuyingRequestPostSucceedController.class)); requestPostSucceedView = new BuyingRequestPostSucceedView(activity, module); activity.setContentView(requestPostSucceedView); } }); } }; @Test public void testOnLoadSomeThings() { final SomeThings products = mock(SomeThings.class); ArrayList<SomeThing> list = mock(ArrayList.class); SomeThing product = mock(SomeThing.class); when(list.get(anyInt())).thenReturn(product); products.productList = list; TestContainerActivity activity = mActivityTestRule.getActivity(); when(list.size()).thenReturn(1); when(list.isEmpty()).thenReturn(false); activity.runOnUiThread(new Runnable() { @Override public void run() { requestPostSucceedView.onLoadSomeThing(products); } }); onView(withId(R.id.id_tips_you_may_also_like_tv)).check(matches(isDisplayed())); // ... }
如上代碼,在TestContainerActivity
啓動後,構造一個mock了Module
的待測試View
,並增長到Activity
的content view中。
若是Viewer
是Activity
,因爲它原本就是Activity,因此它不須要藉助TestContainerActivity
來測試;mock module
時就不能使用構造方法的方式了,由於咱們是不能直接對Activity
進行實例化的,那應該怎麼辦呢?
通常狀況下,咱們會在調用onCreate
方法的時候去進行對依賴的注入,也就是調用XxxYyyZzz_Rapier
擴展類,並且,若是這個Activity
須要在一啓動就去進行一些數據請求,咱們要攔截掉這個請求,由於這個請求返回的數據可能會對咱們的UI測試形成干擾,因此咱們須要在onCreate
在被調用以前把module
mock掉。
首先看test support 中的 ActivityTestRule
這個類,它提供瞭如下幾個方法:
getActivityIntent()
:這個方法只能在Intent中增長攜帶的參數,咱們要mock的是整個Module
,沒法序列化,因此也沒法經過這個傳入。
beforeActivityLaunched()
:這個方法回調時,Activity
實例尚未生成,因此沒法拿到Activity
實例,並進行Module
的替換。
afterActivityFinished()
:這個方法就更不可能了-.-
afterActivityLaunched()
:這個方法看它的源碼(無關代碼已省略):
public T launchActivity(@Nullable Intent startIntent) { // ... beforeActivityLaunched(); // The following cast is correct because the activity we're creating is of the same type as // the one passed in mActivity = mActivityClass.cast(mInstrumentation.startActivitySync(startIntent)); mInstrumentation.waitForIdleSync(); afterActivityLaunched(); return mActivity; }
如上代碼,afterActivityLaunched()
方法是在真正啓動Activity
(mInstrumentation.startActivitySync(startIntent)
)後調用的。可是顯然這個方法是同步的,以後再進入源碼,來查看啓動的流程,整個流程有些複雜我就不贅述了,能夠查看我之前寫的分析啓動流程的博客(http://www.cnblogs.com/tiantianbyconan/p/5017056.html),最後會調用mInstrumentation.callActivityOnCreate(...)
。
可是由於測試時,啓動Activity
的過程也是同步的,因此顯然這個方法是在onCreate()
被調用後纔會被回調的,因此,這個方法也不行。
既然貌似已經找到了mock的正確位置,那就繼續分析下去:
這裏的mInstrumentation
是哪一個Instrumentation
實例呢?
咱們回到ActivityTestRule
中:
public ActivityTestRule(Class<T> activityClass, boolean initialTouchMode, boolean launchActivity) { mActivityClass = activityClass; mInitialTouchMode = initialTouchMode; mLaunchActivity = launchActivity; mInstrumentation = InstrumentationRegistry.getInstrumentation(); }
繼續進入InstrumentationRegistry.getInstrumentation()
:
public static Instrumentation getInstrumentation() { Instrumentation instance = sInstrumentationRef.get(); if (null == instance) { throw new IllegalStateException("No instrumentation registered! " + "Must run under a registering instrumentation."); } return instance; }
繼續查找sInstrumentationRef
是在哪裏set
進去的:
public static void registerInstance(Instrumentation instrumentation, Bundle arguments) { sInstrumentationRef.set(instrumentation); sArguments.set(new Bundle(arguments)); }
繼續查找調用,終於在MonitoringInstrumentation
中找到:
@Override public void onCreate(Bundle arguments) { // ... InstrumentationRegistry.registerInstance(this, arguments); // ... }
因此,測試使用的MonitoringInstrumentation
,而後進入MonitoringInstrumentation
的callActivityOnCreate()
方法:
@Override public void callActivityOnCreate(Activity activity, Bundle bundle) { mLifecycleMonitor.signalLifecycleChange(Stage.PRE_ON_CREATE, activity); super.callActivityOnCreate(activity, bundle); mLifecycleMonitor.signalLifecycleChange(Stage.CREATED, activity); }
既然咱們須要在Activity
真正執行onCreate()
方法時攔截掉,那如上代碼,只要關心signalLifecycleChange()
方法,發現了ActivityLifecycleCallback
的回調:
public void signalLifecycleChange(Stage stage, Activity activity) { // ... Iterator<WeakReference<ActivityLifecycleCallback>> refIter = mCallbacks.iterator(); while (refIter.hasNext()) { ActivityLifecycleCallback callback = refIter.next().get(); if (null == callback) { refIter.remove(); } else { // ... callback.onActivityLifecycleChanged(activity, stage); // ... } }
因此,問題解決了,咱們只要添加一個Activity
生命週期回調就搞定了,代碼以下:
ActivityLifecycleMonitorRegistry.getInstance().addLifecycleCallback(new ActivityLifecycleCallback() { @Override public void onActivityLifecycleChanged(Activity activity, Stage stage) { logger("onActivityLifecycleChanged, activity" + activity + ", stage: " + stage); if(activity instanceof SomethingActivity && Stage.PRE_ON_CREATE == stage){ logger("onActivityLifecycleChanged, got it!!!"); ((SomethingActivity)activity).setModule(mock(SomethingModule.class)); } } });
至此,Activity
的 mock module
成功了。
Presenter
的單元測試與 Viewer
不同,在Presenter
中不該該有Android SDK
相關存在,全部的Inteactor/Api
等都是與Android
解耦的。顯然更加不能有TextView
等存在。正是由於這個,使得它能夠基於PC上的JVM來進行單元測試,也就是說,Presenter
測試不須要Android環境,省去了安裝到手機或者模擬器的步驟。
怎麼去避免Anroid
相關的SDK在Presenter
中存在?
的確有極個別的SDK很難避免,好比Log
。
因此,咱們須要一個XLog
:
public class XLog { private static IXLog delegate; private static boolean DEBUG = true; public static void setDebug(boolean debug) { XLog.DEBUG = debug; } public static void setDelegate(IXLog delegate) { XLog.delegate = delegate; } public static void v(String tag, String msg) { if (DEBUG && null != delegate) { delegate.v(tag, msg); } } public static void v(String tag, String msg, Throwable tr) { if (DEBUG && null != delegate) { delegate.v(tag, msg, tr); } } public static void d(String tag, String msg) { if (DEBUG && null != delegate) { delegate.d(tag, msg); } } // ...
在Android環境中使用的策略:
XLog.setDelegate(new XLogDef());
其中XLogDef
類中的實現爲原生Androd SDK的Log實現。
在測試環境中使用的策略:
logDelegateSpy = Mockito.spy(new XLogJavaTest()); XLog.setDelegate(logDelegateSpy);
其中XLogJavaTest
使用的是純Java的System.out.println()
由於Presenter
中會有不少的異步任務存在,可是在細粒度的單元測試中,沒有異步任務存在的必要性,相應反而增長了測試複雜度。因此,咱們應該把全部異步任務切換成同步操做。
調度的切換使用的是RxJava
,因此全部切換到主線程也是使用了Android SDK
。這裏也要採用策略進行處理。
首先定義了幾種不一樣的ScheduleType
:
public class SchedulerType { public static final int MAIN = 0x3783; public static final int NET = 0x8739; public static final int DB = 0x1385; // ... }
在Schedule
選擇器中根據ScheduleType
進行對應類型的實現:
SchedulerSelector schedulerSelector = SchedulerSelector.get(); schedulerSelector.putScheduler(SchedulerType.MAIN, new SchedulerSelector.SchedulerCreation<Scheduler>() { @Override public Scheduler create() { return AndroidSchedulers.mainThread(); } }); schedulerSelector.putScheduler(SchedulerType.NET, new SchedulerSelector.SchedulerCreation<Scheduler>() { @Override public Scheduler create() { return Schedulers.from(THREAD_POOL_EXECUTOR_NETWORK); } }); schedulerSelector.putScheduler(SchedulerType.DB, new SchedulerSelector.SchedulerCreation<Scheduler>() { @Override public Scheduler create() { return Schedulers.from(THREAD_POOL_EXECUTOR_DATABASE); } }); // ...
當測試時,對調度選擇器中的不一樣類型的實現進行以下替換:
SchedulerSelector.get().putScheduler(SchedulerType.NET, new SchedulerSelector.SchedulerCreation<Scheduler>() { @Override public Scheduler create() { return Schedulers.immediate(); } }); SchedulerSelector.get().putScheduler(SchedulerType.MAIN, new SchedulerSelector.SchedulerCreation<Scheduler>() { @Override public Scheduler create() { return Schedulers.immediate(); } });
把全部調度都改爲當前線程執行便可。
最後Presenter
測試幾個範例:
@Mock AccountContract.IAccountViewer viewer; @Mock UserInteractor userInteractor; AccountPresenter presenter; @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); presenter = new AccountPresenter(viewer); presenter.userInteractor = userInteractor; } @Test public void requestEditUserInfo() throws Exception { // case 1, succeed reset(viewer); resetLog(); when(userInteractor.requestEditUserInfo(any(User.class))).thenReturn(Observable.just(anyBoolean())); presenter.requestEditUserInfo(new User()); verifyOnce(viewer).onRequestEditUserInfo(); // case 2, null reset(viewer); resetLog(); when(userInteractor.requestEditUserInfo(any(User.class))).thenReturn(Observable.just(null)); presenter.requestEditUserInfo(new User()); verifyOnce(viewer).onRequestEditUserInfo(); // case 3, error assertFailedAndError(() -> userInteractor.requestEditUserInfo(any(User.class)), () -> presenter.requestEditUserInfo(new User())); }
public class SBuyingRequestPostSucceedViewPresenterTest extends BaseJavaTest { @Mock public IBuyingRequestPostSucceedView viewer; @Mock public BuyingRequestPostSucceedPresenterModule module; @Mock public ApiSearcher apiSearcher; public IBuyingRequestPostSucceedPresenter presenter; @Before public void setUp() { MockitoAnnotations.initMocks(this); when(module.pickApiSearcher()).thenReturn(apiSearcher); presenter = new BuyingRequestPostSucceedPresenter(viewer, module); } @Test public void testLoadSomethingSuccess() throws TimeoutException { // Mock success observable when(apiSearcher.searcherSomething(anyString(), anyString(), anyString())) .thenReturn(Observable.create(new Observable.OnSubscribe<OceanServerResponse<Something>>() { @Override public void call(Subscriber<? super OceanServerResponse<Something>> subscriber) { try { OceanServerResponse<Something> oceanServerResponse = mock(OceanServerResponse.class); when(oceanServerResponse.getBody(any(Class.class))).thenReturn(mock(Something.class)); subscriber.onNext(oceanServerResponse); subscriber.onCompleted(); } catch (Throwable throwable) { subscriber.onError(throwable); } }I })); final ExecuteStuff executeStuff = new ExecuteStuff(); Answer succeedAnswer = new Answer() { @Override public Object answer(InvocationOnMock invocationOnMock) throws Throwable { loggerMockAnswer(invocationOnMock); executeStuff.setSucceed(true); return null; } }; doAnswer(succeedAnswer).when(viewer).onLoadSomething(Matchers.any(Something.class)); presenter.loadSomething("whatever", "whatever"); logger("loadSomething result: " + executeStuff.isSucceed()); Assert.assertTrue("testLoadSomethingSuccess result true", executeStuff.isSucceed()); } @Test public void testLoadSomethingFailed() throws TimeoutException { // Mock error observable when(apiSearcher.searcherRFQInterestedProductsSuggestion(anyString(), anyString(), anyString())) .thenReturn(Observable.<OceanServerResponse<Something>>error(new RuntimeException("mock error observable"))); final ExecuteStuff executeStuff = new ExecuteStuff(); Answer failedAnswer = new Answer() { @Override public Object answer(InvocationOnMock invocationOnMock) throws Throwable { loggerMockAnswer(invocationOnMock); executeStuff.setSucceed(false); return null; } }; doAnswerWhenLogError(failedAnswer); presenter.loadSomething("whatever", "whatever"); logger("testLoadSomethingFailed result: " + executeStuff.isSucceed()); Assert.assertFalse("testLoadSomethingFailed result false", executeStuff.isSucceed()); } }