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

来源:http://www.cnblogs.com/tiantianbyconan/archive/2016/04/22/5422443.html
-Advertisement-
Play Games

以下內容為原創,歡迎轉載,轉載請註明 來自天天博客: 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中的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生命周期回調進行抽象:

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層交互,所以應該還提供對這兩者的綁定操作,所以如下:

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

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的具體實現。

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對象,然後調用Controllerbind方法進行生命周期的綁定。

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層,作為溝通 ViewModel 的橋梁,它從 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()方法中,把對應的SubscriptionSet中移除掉。

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 進行依賴註入

上面提到,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,如下:

public class TestContainerActivity extends BaseActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }
}

記得在AndroidManifest.xml中註冊。

前面說過,我們需要mock掉Module

如果ViewerView,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

如果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():這個方法看它的源碼(無關代碼已省略):

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中:

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,然後進入MonitoringInstrumentationcallActivityOnCreate()方法:

@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());

    }

}

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 問題描述: 今天是我決定專註Android開發的第一天,我在網上下載了一個數獨游戲的源碼,準備開始著手學習。在導入之後出現Java文件中import *.R文件報錯,在gen目錄下麵沒有找到R.java,於是自己創建了個,然後開始了尋找自動更新的方法。 問題解決: 在經過尋找後,最後發現是自己的項目 ...
  • ios開發學習中,經常弄不清楚ios的開發模式,今天我們就來進行簡單的總結和探討~ (一)代理模式 應用場景:當一個類的某些功能需要由別的類來實現,但是又不確定具體會是哪個類實現。 優勢:解耦合 敏捷原則:開放-封閉原則 實例:tableview的 數據源delegate,通過和protocol的配 ...
  • 使用NSURLSessionDownloadTask下載文件的過程與前面差不多,需要註意的是文件下載文件之後會自動保存到一個臨時目錄,需要開發人員自己將此文件重新放到其他指定的目錄中。 來自KenshinCui,鏈接見上篇。 ...
  • AFN小結 1,AFN概念、原理 2,AFN的封裝使用 3,AFN與其它框架對比 ————————————————————————————————— 1 , AFN的概念原理: AFN的基礎是NSURL,AFN的直接操作對象AFHTTPClient是一個實現了NSCoding和NSCopying協議 ...
  • 最近突然發現我的128G SSD硬碟只剩下可憐的8G多,剩下這麼少的一點空間連Xcode都無法更新。怎麼辦呢?如果升級硬碟的話,第一要花錢,畢竟SSD硬碟還是不便宜,第二是升級比較麻煩,要拆機和遷移系統什麼的特別花時間精力,老了真不願瞎折騰了,只能想辦法能不能清除點空間來。 尋找大塊頭 首先想到的就 ...
  • 一 KVC的基本概念 KVC是Key Value Coding的縮寫,意思是鍵值編碼。 在iOS中,提供了一種方法通過使用屬性的名稱(也就是Key)來間接訪問對象屬性的方法,這個方法可以不通過getter/setter方法來訪問對象的屬性。用KVC可以間接訪問對象屬性的機制。通常我們使用valueF ...
  • // 註意,在有導航欄的情況下,需要在viewDidLoad 中加上 self.automaticallyAdjustsScrollViewInsets = NO;不然會出現圖片下移64的情況 ,scrollView.frame的高度要比圖片的高度大64,不然,圖片顯示不全 源文件在這裡:http: ...
  • Socket小白篇-附加TCP/UDP簡介 1.Socket 什麼是Socket Socket:又稱作是套接字,網路上的兩個程式通過一個雙向的通信連接實現數據的交換,這個連接的一端稱為Socket。 Socket是對TCP/IP的協議的封裝,Socket本身並不是協議,而是一個調用的介面,只有通過S ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...