[Android]Android MVP&依赖注入&单元测试

简介: 以下内容为原创,欢迎转载,转载请注明来自天天博客:http://www.cnblogs.com/tiantianbyconan/p/5422443.htmlAndroid MVP&依赖注入&单元测试注意:为了区分MVP中的View与Android中控件的View,以下MVP中的View使用Viewer来表示。


以下内容为原创,欢迎转载,转载请注明
来自天天博客: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测试几个范例:

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

    }

}
相关文章
|
Java Android开发
Android如何通过Gradle发布Android依赖库(aar)到 jitpack 公共仓库
Android如何通过Gradle发布Android依赖库(aar)到 jitpack 公共仓库
635 0
|
3月前
|
开发工具 Android开发 git
Android实战之组件化中如何进行版本控制和依赖管理
本文介绍了 Git Submodules 的功能及其在组件化开发中的应用。Submodules 允许将一个 Git 仓库作为另一个仓库的子目录,有助于保持模块独立、代码重用和版本控制。虽然存在一些缺点,如增加复杂性和初始化时间,但通过最佳实践可以有效利用其优势。
52 3
|
Android开发
【错误记录】Android Studio 编译报错 ( Error:Connection timed out: connect | 更新配置依赖仓库方式 )
【错误记录】Android Studio 编译报错 ( Error:Connection timed out: connect | 更新配置依赖仓库方式 )
974 0
【错误记录】Android Studio 编译报错 ( Error:Connection timed out: connect | 更新配置依赖仓库方式 )
|
7月前
|
Java API 开发工具
解决 Android 依赖冲突
解决 Android 依赖冲突
126 0
|
Java Maven Android开发
android之gradle配置仓库与引入依赖
android之gradle配置仓库与引入依赖
648 0
|
Android开发
Android 开发引用 okio 依赖之后无法运行main方法的坑
Android 开发引用 okio 依赖之后无法运行main方法的坑
109 1
|
Android开发
【Android 插件化】VirtualApp 安装并启动资源中自带的 APK 插件 ( 添加依赖库 | 准备插件 APK | 启动插件引擎 | 拷贝 APK 插件 | 安装插件 | 启动插件 )(二)
【Android 插件化】VirtualApp 安装并启动资源中自带的 APK 插件 ( 添加依赖库 | 准备插件 APK | 启动插件引擎 | 拷贝 APK 插件 | 安装插件 | 启动插件 )(二)
475 0
【Android 插件化】VirtualApp 安装并启动资源中自带的 APK 插件 ( 添加依赖库 | 准备插件 APK | 启动插件引擎 | 拷贝 APK 插件 | 安装插件 | 启动插件 )(二)
|
XML 传感器 Java
Android 天气APP(三十六)运行到本地AS、更新项目版本依赖、去掉ButterKnife
最近发现这个项目好似迎来了第二春,GitHub上的Start和Fork增加的很快,我的猜测是学生在通过这个项目来学习和完成自己的作业。随着Android版本的更新,Android Studio的更新,项目中一些内容不能在新版本中很好的使用,甚至出现编译不了的情况,这对我来说没啥,但是对于拿到项目满心欢喜的学生来说,运行不了,无疑是致命的,而我写在项目Readme下面我将演示一下怎么在本地运行这个项目的代码。
433 1
Android 天气APP(三十六)运行到本地AS、更新项目版本依赖、去掉ButterKnife
|
Android开发
【安卓开发】AndroidStudio添加依赖
【安卓开发】AndroidStudio添加依赖
107 0
【安卓开发】AndroidStudio添加依赖
|
Android开发
android 常用的依赖
android 常用的依赖