天天看點

[Android]Android MVP&依賴注入&單元測試Android MVP&依賴注入&單元測試

以下内容為原創,歡迎轉載,轉載請注明

注意:為了區分<code>MVP</code>中的<code>View</code>與<code>Android</code>中控件的<code>View</code>,以下<code>MVP</code>中的<code>View</code>使用<code>Viewer</code>來表示。

這裡暫時先隻讨論 <code>Viewer</code> 和 <code>Presenter</code>,<code>Model</code>暫時不去涉及。

首先需要解決以下問題:

<code>MVP</code>中把Layout布局和<code>Activity</code>等元件作為<code>Viewer</code>層,增加了<code>Presenter</code>,<code>Presenter</code>層與<code>Model</code>層進行業務的互動,完成後再與<code>Viewer</code>層互動,進行回調來重新整理UI。這樣一來,業務邏輯的工作都交給了<code>Presenter</code>中進行,使得<code>Viewer</code>層與<code>Model</code>層的耦合度降低,<code>Viewer</code>中的工作也進行了簡化。但是在實際項目中,随着邏輯的複雜度越來越大,<code>Viewer</code>(如<code>Activity</code>)臃腫的缺點仍然展現出來了,因為<code>Activity</code>中還是充滿了大量與<code>Viewer</code>層無關的代碼,比如各種事件的處理派發,就如<code>MVC</code>中的那樣<code>Viewer</code>層和<code>Controller</code>代碼耦合在一起無法自拔。

解決的方法之一在上述文章中也有提到 —— 加入<code>Controller</code>層來分擔<code>Viewer</code>的職責。

根據以上的解決方案,首先考慮到<code>Viewer</code>直接互動的對象可能是<code>Presenter</code>(原來的方式),也有可能是<code>Controller</code>。

如果直接互動的對象是<code>Presenter</code>,由于<code>Presenter</code>中可能會進行很多同步、異步操作來調用<code>Model</code>層的代碼,并且會回調到UI來進行UI的更新,是以,我們需要在<code>Viewer</code>層對象銷毀時能夠停止<code>Presenter</code>中執行的任務,或者執行完成後攔截UI的相關回調。是以,<code>Presenter</code>中應該綁定<code>Viewer</code>對象的生命周期(至少<code>Viewer</code>銷毀的生命周期是需要關心的)

如果直接互動的對象是<code>Controller</code>,由于<code>Controller</code>中會承擔<code>Viewer</code>中的事件回調并派發的職責(比如,ListView item 的點選回調和點選之後對相應的邏輯進行派發、或者<code>Viewer</code>生命周期方法回調後的處理),是以<code>Controller</code>層也是需要綁定<code>Viewer</code>對象的生命周期的。

這裡,使用<code>Viewer</code>生命周期回調進行抽象:

<code>OnViewerDestroyListener</code>接口提供給需要關心<code>Viewer</code>層銷毀時期的元件,如上,應該是<code>Presenter</code>所需要關心的。

<code>OnViewerLifecycleListener</code>接口提供給需要關心<code>Viewer</code>層生命周期回調的元件,可以根據項目需求增加更多的生命周期的方法,這裡我們隻關心<code>Viewer</code>的<code>resume</code>和<code>pause</code>。

<code>Viewer</code>層,也就是表現層,當然有相關常用的UI操作,比如顯示一個<code>toast</code>、顯示/取消一個加載進度條等等。除此之外,由于<code>Viewer</code>層可能會直接與<code>Presenter</code>或者<code>Controller</code>層互動,是以應該還提供對這兩者的綁定操作,是以如下:

如上代碼,兩個<code>bind()</code>方法就是用于跟<code>Presenter</code>/<code>Controller</code>的綁定。

又因為,在Android中<code>Viewer</code>層對象可能是<code>Activity</code>、<code>Fragment</code>、<code>View</code>(包括<code>ViewGroup</code>),甚至還有自己實作的元件,當然實作的方式一般不外乎上面這幾種。是以我們需要使用統一的<code>Activity</code>、<code>Fragment</code>、<code>View</code>,每個都需要實作<code>Viewer</code>接口。為了複用相關代碼,這裡提供預設的委托實作<code>ViewerDelegate</code>:

如上代碼:

它提供了預設基本的<code>toast</code>、和顯示/隐藏加載進度條的方法。

它實作了兩個重載<code>bind()</code>方法,并把需要回調的<code>OnViewerLifecycleListener</code>和<code>OnViewerDestroyListener</code>對應儲存在<code>mOnViewerDestroyListeners</code>和<code>mOnViewerLifecycleListeners</code>中。

它實作了<code>OnViewerLifecycleListener</code>接口,在回調方法中回調到每個<code>mOnViewerDestroyListeners</code>和<code>mOnViewerLifecycleListeners</code>。

<code>mOnViewerDestroyListeners</code>:Viewer destroy 時的回調,一般情況下隻會有Presenter一個對象,但是由于一個Viewer是可以有多個Presenter的,是以可能會維護一個Presenter清單,還有可能是其他需要關心 Viewer destroy 的元件

<code>mOnViewerLifecycleListeners</code>:Viewer 簡單的生命周期監聽對象,一般情況下隻有一個Controller一個對象,但是一個Viewer并不限制隻有一個Controller對象,是以可能會維護一個Controller清單,還有可能是其他關心 Viewer 簡單生命周期的元件

然後在真實的<code>Viewer</code>中(這裡以<code>Activity</code>為例,其他<code>Fragment</code>/<code>View</code>等也是一樣),首先,應該實作<code>Viewer</code>接口,并且應該維護一個委托對象<code>mViewerDelegate</code>,在實作的<code>Viewer</code>方法中使用<code>mViewerDelegate</code>的具體實作。

如上,<code>BaseActivity</code>建構完成。

在具體真實的<code>Viewer</code>實作中,包含的方法應該都是類似<code>onXxxYyyZzz()</code>的回調方法,并且這些回調方法應該隻進行UI操作,比如<code>onLoadMessage(List&lt;Message&gt; message)</code>方法在加載完<code>Message</code>資料後回調該方法來進行UI的更新。

在項目中使用時,應該使用依賴注入來把<code>Controller</code>對象注入到<code>Viewer</code>中(這個後面會提到)。

使用<code>RInject</code>通過<code>BuyingRequestPostSucceedView_Rapier</code>擴充類來進行注入<code>Controller</code>對象,然後調用<code>Controller</code>的<code>bind</code>方法進行生命周期的綁定。

前面講過,<code>Controller</code>是需要關心<code>Viewer</code>生命周期的,是以需要實作<code>OnViewerLifecycleListener</code>接口。

又提供一個<code>bind()</code>方法來進行對自身進行綁定到對應的<code>Viewer</code>上面。

調用<code>Viewer</code>層的<code>bind()</code>方法來進行綁定,對生命周期進行空實作。

該<code>bind()</code>方法除了用于綁定<code>Viewer</code>之外,還可以讓子類重寫用于做為Controller的初始化方法,但是注意重寫的時候必須要調用<code>super.bind()</code>。

具體<code>Controller</code>實作中,應該隻包含類似<code>onXxxYyyZzz()</code>的回調方法,并且這些回調方法應該都是各種事件回調,比如<code>onClick()</code>用于View點選事件的回調,<code>onItemClick()</code>表示AdapterView item點選事件的回調。

<code>Presenter</code>層,作為溝通 <code>View</code> 和 <code>Model</code> 的橋梁,它從 <code>Model</code> 層檢索資料後,傳回給 <code>View</code> 層,它也可以決定與 <code>View</code> 層的互動操作。

前面講到過,<code>View</code>也是與<code>Presenter</code>直接互動的,Presenter中可能會進行很多同步、異步操作來調用Model層的代碼,并且會回調到UI來進行UI的更新,是以,我們需要在Viewer層對象銷毀時能夠停止Presenter中執行的任務,或者執行完成後攔截UI的相關回調。

是以:

<code>Presenter</code> 中應該也有<code>bind()</code>方法來進行與<code>Viewer</code>層的生命周期的綁定

<code>Presenter</code> 中應該提供一個方法<code>closeAllTask()</code>來終止或攔截掉UI相關的異步任務。

如下:

因為項目技術需求,需要實作對<code>RxJava</code>的支援,是以,這裡對<code>Presenter</code>進行相關的擴充,提供兩個方法以便于<code>Presenter</code>對任務的擴充。

<code>goSubscription()</code>方法主要用處是,訂閱時緩存該訂閱對象到<code>Presenter</code>中,便于管理(怎麼管理,下面會講到)。

<code>removeSubscription()</code>方法可以從<code>Presenter</code>中管理的訂閱緩存中移除掉該訂閱。

在Presenter RxJava 實作(<code>RxBasePresenter</code>)中,我們使用<code>WeakHashMap</code>來建構一個弱引用的<code>Set</code>,用它來緩存所有訂閱。在調用<code>goSubscription()</code>方法中,把對應的<code>Subscription</code>加入到<code>Set</code>中,在<code>removeSubscription()</code>方法中,把對應的<code>Subscription</code>從<code>Set</code>中移除掉。

如上代碼,在<code>onViewerDestroy()</code>回調時(因為跟<code>Viewer</code>生命周期進行了綁定),會調用<code>closeAllTask</code>把所有緩存中的<code>Subscription</code>取消訂閱。

注意:因為緩存中使用了弱引用,是以上面的<code>removeSubscription</code>不需要再去手動調用,在訂閱completed後,gc自然會回收掉沒有強引用指向的<code>Subscription</code>對象。

在<code>Presenter</code>具體的實作中,同樣依賴注入各種來自<code>Model</code>層的<code>Interactor/Api</code>(網絡、資料庫、檔案等等),然後訂閱這些對象傳回的<code>Observable</code>,然後進行訂閱,并調用<code>goSubscription()</code>緩存<code>Subscription</code>:

暫不讨論。

上面提到,<code>Viewer</code>、<code>Controller</code>和<code>Presenter</code>中都使用了<code>RInject</code>注解來進行依賴的注入。

之後再針對這個<code>Rapier</code>庫進行詳細讨論。

這裡主要還是讨論針對<code>Viewer</code>和<code>Presenter</code>的單元測試。

針對<code>Viewer</code>進行單元測試,這裡不涉及任何業務相關的邏輯,而且,<code>Viewer</code>層的測試都是UI相關,必須要Android環境,是以需要在手機或者模拟器安裝一個<code>test</code> apk,然後進行測試。

為了不被<code>Viewer</code>中的<code>Controller</code>和<code>Presenter</code>的邏輯所幹擾,我們必須要mock掉<code>Viewer</code>中的<code>Controller</code>和<code>Presenter</code>對象,又因為<code>Controller</code>對象是通過依賴注入的方式提供的,也就是來自<code>Rapier</code>中的<code>Module</code>,是以,我們隻需要mock掉<code>Viewer</code>對應的<code>module</code>。

如果<code>Viewer</code>層是由<code>View</code>實作的,比如繼承<code>FrameLayout</code>。這個時候,測試時,就必須要放在一個<code>Activity</code>中測試(<code>Fragment</code>也一樣,也必須依賴于<code>Activity</code>),是以我們應該有一個專門用于測試<code>View/Fragment</code>的<code>Activity</code> —— <code>TestContainerActivity</code>,如下:

記得在<code>AndroidManifest.xml</code>中注冊。

前面說過,我們需要mock掉<code>Module</code>。

如果<code>Viewer</code>是<code>View</code>,mock掉<code>Module</code>就非常容易了,隻要在<code>View</code>中提供一個傳入mock的<code>Module</code>的構造方法即可,如下:

如上代碼,這裡為測試專門提供了一個構造方法來進行對<code>Module</code>的mock,之後的測試如下:

如上代碼,在<code>TestContainerActivity</code>啟動後,構造一個mock了<code>Module</code>的待測試<code>View</code>,并增加到<code>Activity</code>的content view中。

如果<code>Viewer</code>是<code>Activity</code>,由于它本來就是Activity,是以它不需要借助<code>TestContainerActivity</code>來測試;mock <code>module</code>時就不能使用構造方法的方式了,因為我們是不能直接對<code>Activity</code>進行執行個體化的,那應該怎麼辦呢?

一般情況下,我們會在調用<code>onCreate</code>方法的時候去進行對依賴的注入,也就是調用<code>XxxYyyZzz_Rapier</code>擴充類,而且,如果這個<code>Activity</code>需要在一啟動就去進行一些資料請求,我們要攔截掉這個請求,因為這個請求傳回的資料可能會對我們的UI測試造成幹擾,是以我們需要在<code>onCreate</code>在被調用之前把<code>module</code> mock掉。

首先看test support 中的 <code>ActivityTestRule</code>這個類,它提供了以下幾個方法:

<code>getActivityIntent()</code>:這個方法隻能在Intent中增加攜帶的參數,我們要mock的是整個<code>Module</code>,無法序列化,是以也無法通過這個傳入。

<code>beforeActivityLaunched()</code>:這個方法回調時,<code>Activity</code>執行個體還沒有生成,是以無法拿到<code>Activity</code>執行個體,并進行<code>Module</code>的替換。

<code>afterActivityFinished()</code>:這個方法就更不可能了-.-

<code>afterActivityLaunched()</code>:這個方法看它的源碼(無關代碼已省略):

但是因為測試時,啟動<code>Activity</code>的過程也是同步的,是以顯然這個方法是在<code>onCreate()</code>被調用後才會被回調的,是以,這個方法也不行。

既然貌似已經找到了mock的正确位置,那就繼續分析下去:

這裡的<code>mInstrumentation</code>是哪個<code>Instrumentation</code>執行個體呢?

我們回到<code>ActivityTestRule</code>中:

繼續進入<code>InstrumentationRegistry.getInstrumentation()</code>:

繼續查找<code>sInstrumentationRef</code>是在哪裡<code>set</code>進去的:

繼續查找調用,終于在<code>MonitoringInstrumentation</code>中找到:

是以,測試使用的<code>MonitoringInstrumentation</code>,然後進入<code>MonitoringInstrumentation</code>的<code>callActivityOnCreate()</code>方法:

既然我們需要在<code>Activity</code>真正執行<code>onCreate()</code>方法時攔截掉,那如上代碼,隻要關心<code>signalLifecycleChange()</code>方法,發現了<code>ActivityLifecycleCallback</code>的回調:

是以,問題解決了,我們隻要添加一個<code>Activity</code>生命周期回調就搞定了,代碼如下:

至此,<code>Activity</code>的 mock <code>module</code>成功了。

<code>Presenter</code> 的單元測試與 <code>Viewer</code> 不一樣,在<code>Presenter</code>中不應該有<code>Android SDK</code>相關存在,所有的<code>Inteactor/Api</code>等都是與<code>Android</code>解耦的。顯然更加不能有<code>TextView</code>等存在。正是因為這個,使得它可以基于PC上的JVM來進行單元測試,也就是說,<code>Presenter</code>測試不需要Android環境,省去了安裝到手機或者模拟器的步驟。

怎麼去避免<code>Anroid</code>相關的SDK在<code>Presenter</code>中存在?

的确有極個别的SDK很難避免,比如<code>Log</code>。

是以,我們需要一個<code>XLog</code>:

在Android環境中使用的政策:

其中<code>XLogDef</code>類中的實作為原生Androd SDK的Log實作。

在測試環境中使用的政策:

其中<code>XLogJavaTest</code>使用的是純Java的<code>System.out.println()</code>

因為<code>Presenter</code>中會有很多的異步任務存在,但是在細粒度的單元測試中,沒有異步任務存在的必要性,相應反而增加了測試複雜度。是以,我們應該把所有異步任務切換成同步操作。

排程的切換使用的是<code>RxJava</code>,是以所有切換到主線程也是使用了<code>Android SDK</code>。這裡也要采用政策進行處理。

首先定義了幾種不同的<code>ScheduleType</code>:

在<code>Schedule</code>選擇器中根據<code>ScheduleType</code>進行對應類型的實作:

當測試時,對排程選擇器中的不同類型的實作進行如下替換:

把所有排程都改成目前線程執行即可。

最後<code>Presenter</code>測試幾個範例: