Android单元测试—逻辑测试

前言

在之前的系列博客中,主要围绕的是测试工具的介绍与使用。经过几个月的沉寂,在项目中摸索与实践单元测试,曾经踩坑无数,自己从中受益匪浅,确实是一段成长的历程!今天准备一些干货,给感兴趣的同学借鉴一下,主要是分享在项目实践过程中的经验总结以及对Android单元测试的理解,将以两篇博客的篇幅进行详细介绍,欢迎大家关注!

先上个图压压惊

Precondition

需要明确的是,单元测试分为两部分,即UI测试和逻辑测试,其两者的实现方式是有所不同的,效率也是不一样的。现在的项目中,大都使用MVP设计框架,它通过面向接口编程的方式,借助于Presenter这个中间层从而实现View层和Model层的隔离,不仅方便项目维护扩展,因其把依赖于Android环境的View层和纯Java的数据逻辑处理层分离,还方便我们进行单元测试。工欲善其事,必先利其器,在实践之前,我们要用MVP设计框架对项目进行重构,只有建立在良好的架构和明确的层次,单元测试实施起来才能事半功倍。

MVP

先说UI测试,对应于MVP设计框架中的View层,所写的Case代码位于src/androidTest/java/。既然是Android的UI,就依赖于Android环境,那么我们针对这个的单元测试覆盖也就需要运行在Android虚拟机和Android真机上,想必你也知道,每当我们Run一次都需要好几分钟的等待时间,期间经过编译成apk,并把apk安装在Android环境上。这就是为什么我们要把项目分为UI测试和逻辑测试,因为耗时。对Android UI测试,想必你可能了解,Google官方推出了Espresso,使用起来很方便,会在以后的博客中展开来说。

而逻辑测试,对应于MVP设计框架的Presenter层和Model层,所写的Case代码位于src/test/java/。指的是纯Java代码的单元覆盖,比如说登录时对账户密码合法性的校验逻辑,再比如说是数据的请求、存储、封装等处理逻辑,这部分的代码往往不依赖于Android环境,可能会对Android Context上下文的依赖,相对UI来说要纯粹一些。看过之前博客的同学可能会知道,强烈推荐使用测试框架PowerMockito+Robolectric

(1)、PowerMockito不仅可以mock Public数据对象,还可以mock Private、Final、Static、Singleton等数据对象,通过Mock数据对象的方式可以帮助我们隔离外部依赖,让我们只专注于目标代码输入输出等调用逻辑的测试;
(2)、Robolectric通过实现一套能在JVM能运行的Android代码,为我们提供Android Application和Context的支持,因为在Model层需要依赖于Android Context上下文,比如说对Android数据库Sqlite操作和SharedPreference等数据存储操作。

逻辑测试

今天的重点是分享如何进行逻辑代码的单元覆盖,终于说到正题了。

build.gradle配置:
    testCompile 'junit:junit:4.12'
    testCompile 'org.assertj:assertj-core:1.7.0'
    testCompile 'org.robolectric:robolectric:3.0'
    testCompile 'org.powermock:powermock-module-junit4:1.6.5'
    testCompile 'org.powermock:powermock-module-junit4-rule:1.6.5'
    testCompile 'org.powermock:powermock-api-mockito:1.6.5'
    testCompile 'org.powermock:powermock-classloading-xstream:1.6.5'

细心的同学会发现,此处robolectric用的是老版本3.0,并没有用最新的版本3.3。前方高能,从github的反馈中看出,新版本有坑还不稳定。如果项目中需要读取配置信息(如HTTPS的证书、预置数据),就得使用assets文件。默认情况下,robolectric3.0版本无法读取asset文件,还得自定义RobolectricTestRunner

自定义Runner
public class CustomTestRunner extends RobolectricTestRunner {

    private static final String APP_MODULE_NAME = "app";

    /**
     * Creates a runner to run {@code testClass}. Looks in your working directory for your AndroidManifest.xml file
     * and res directory by default. Use the {@link org.robolectric.annotation.Config} annotation to configure.
     *
     * @param testClass the test class to be run
     * @throws org.junit.runners.model.InitializationError if junit says so
     */
    public CustomTestRunner(Class<?> testClass) throws InitializationError {
        super(testClass);
    }

    @Override
    protected AndroidManifest getAppManifest(Config config) {

        String userDir = System.getProperty("user.dir", "./");
        File current = new File(userDir);
        String prefix;
        if (new File(current, APP_MODULE_NAME).exists()) {
            System.out.println("Probably running on AndroidStudio");
            prefix = "./" + APP_MODULE_NAME;
        } else if (new File(current.getParentFile(), APP_MODULE_NAME).exists()) {
            System.out.println("Probably running on Console");
            prefix = "../" + APP_MODULE_NAME;
        } else {
            throw new IllegalStateException("Could not find app module, app module should be \"app\" directory in the project.");
        }
        System.setProperty("android.manifest", prefix + "/src/main/AndroidManifest.xml");
        System.setProperty("android.resources", prefix + "/src/main/res");
        System.setProperty("android.assets", prefix + "/src/main/assets");

        return new AndroidManifest(Fs.fileFromPath(prefix + "/src/main/AndroidManifest.xml"), Fs.fileFromPath(prefix + "/src/main/res"), Fs.fileFromPath(prefix + "/src/main/assets")) {
            @Override
            public int getTargetSdkVersion() {
                return 18;
            }
        };
    }

}

在代码末尾处,你会发现下面代码:

public int getTargetSdkVersion() {
     return 18;
}

一个非常重要的细节,若是不重写指定Android版本的话,就会报错java.lang.UnsupportedOperationException: Robolectric does not support API level 1, sorry!,然而在最新的robolectric版本没有这个Exception。说点题外话,除了重写getTargetSdkVersion方法这种方式,还可以在AndroidManifest.xml配置文件中指定compileSdkVersion,虽然可以解决这个Exception,但是你不觉得这种方式侵入性有点大吗,在Android Studio中配置sdk版本是在gradle文件中配置,所以不推荐这种方式。

BaseRoboTestCase

避免重复代码,定义抽象类BaseRoboTestCase,只要继承重写就可以开始单元测试之旅,是不是很方便呀!

@Config( shadows = {ShadowLog.class})
@RunWith(CustomTestRunner.class)
@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*", "org.json.*", "sun.security.*", "javax.net.*"})
public abstract class BaseRoboTestCase {
    @Rule
    public PowerMockRule rule = new PowerMockRule();
    private static boolean hasInitRxJava = false;

    @Before
    public void setUp() {
        ShadowLog.stream = System.out;
        System.out.println("setUp now");
        Robolectric.getShadowApplication();
        if (!hasInitRxJava) {
            hasInitRxJava = true;
            initRxJava();
        }
        MockitoAnnotations.initMocks(this);
    }

    public Application getApplication() {
        return Robolectric.application;
    }

    public Context getContext() {
        return getApplication();
    }

    private void initRxJava() {
        RxJavaPlugins.getInstance().registerSchedulersHook(new RxJavaSchedulersHook() {
            @Override
            public Scheduler getIOScheduler() {
                return Schedulers.immediate();
            }
        });
        RxAndroidPlugins.getInstance().registerSchedulersHook(new RxAndroidSchedulersHook() {
            @Override
            public Scheduler getMainThreadScheduler() {
                return Schedulers.immediate();
            }
        });
    }

    @Test
    public void test() {
    }

}

上面代码涉及的知识会有点多,在这里我们只关注重点,更加详细的可以参考我之前写的博客

1、通过@RunWith(CustomTestRunner.class)方式注入上面说到的自定义Runner。
2、不知道你注意到了没有,上面写了一个空的test()测试方法,方法名可以随意定义,这是为啥呢?是因为在终端上运行./gradlew testDebugUnitTest --continue指令批量来跑src/test/java/目录下所有的单元测试Case时,会抛出异常java.lang.Exception: No runnable methods
3、公司项目使用的是RxJava+Retrofit+OKHttp框架来处理网络请求和异步操作的,在对RxJava相关的代码进行单元测试时,线程切换是非常重要。RxJava官方考虑到单元测试,为我们提供了Hook的方式来保证线程切换,通过RxAndroidPlugins.getInstance().registerSchedulersHook()方法可以将其他线程的处理统一切换到我们指定线程Schedulers.immediate()来处理,即当前单元测试跑的这个线程,如此一来方便单元测试验证。

写好Presenter

MVP设计框架中,如何写好Presenter层,是一个很有艺术的问题。想当初初学MVP时,还是会按照之前MVC的惯性思维,会把部分的数据逻辑(比如说数据对象空、越界、合法性等判断)处理放在Activity中,这样导致的结果是,如果想单元测试这部分逻辑代码,就会显得比较麻烦,必须得在Android测试环境下执行。其实,一个好的Presenter层应该是,包含绝大部分的数据处理逻辑,而View层只执行UI的更新工作(setText、setVisibile、setFocus等),如此一来就很方便我们进行单元覆盖Pressenter所有逻辑分支。换句话说,Presenter层直接影响到纯Java代码的覆盖率了,进而关系到bug率。

隔离外部依赖

一个很普遍的问题是,要测试的目标类会有很多外部依赖,这些依赖的类/对象/资源又会有别的依赖,从而形成一个大的依赖树,要在单元测试的环境中完整地构建这样的依赖,是一件很困难的事情。而通过Mock的方式,对测试的类所依赖的其他类和对象,进行mock构建假对象,并定义这些假对象上的行为,然后提供给被测试对象使用。被测试对象像使用真的对象一样使用它们。用这种方式,我们可以把测试的目标限定于被测试对象本身,就如同在被测试对象周围做了一个划断,形成了一个尽量小的被测试目标。

但Mock的前提是你的代码可以进行外部依赖注入,可能我们在不知觉中,就会在类中构造并定义私有变量,或者在用到的时候直接new,让我们没法方便进行依赖注入,诸如此类都不是正确的姿势。如下:

外部依赖错误的使用姿势

所以在coding时,对于外部依赖,尽量要提供接口可以注入依赖,否则我们难以入手。可以通过构造函数的方式传入外部依赖,也可以通过set方法,要是项目使用Dagger2框架,可以通过依赖注解的方式解决。正确的姿势如下:

外部依赖正确的使用姿势

测试普通方法

当我们要对一个方法进行测试时,该如何下手呢?

  1. 有明确的返回值,做单元测试时,只需调用这个函数,验证其返回值是否符合预期结果,这个很简单。
  2. 对于无返回值的void方法,这个方法只改变其对象内部的一些属性或者状态,就验证它所改变的属性和状态,可以通过ArgumentCaptor方式来捕获并验证中间状态,也可以验证是否执行外部依赖的方法。

测试异步方法

深切体会到,测试异步方法,是整个单元测试的难点和重点,为什么这么说呢?问题很明显,当测试方法跑完了的时候,被测的异步代码可能还在执行没跑完,这就有问题了。再者就是实现异步操作的框架比较多样。下面有这么一个AyncModel类:

public class AyncModel {

    private Handler mUiHandler = new Handler(Looper.getMainLooper());

    public void loadAync(final Callback callback) {
        new Thread(new Runnable() {

            @Override
            public void run() {
                try {
                    // 模拟耗时操作
                    Thread.sleep(1000);
                    final List<String> results = new ArrayList<>();
                    results.add("test String");
                    mUiHandler.post(new Runnable() {
                        @Override
                        public void run() {
                            callback.onSuccess(results);
                        }
                    });
                } catch (final InterruptedException e) {
                    e.printStackTrace();
                    mUiHandler.post(new Runnable() {
                        @Override
                        public void run() {
                            callback.onFailure(500, e.getMessage());
                        }
                    });
                }
            }
        }).start();
    }

    interface Callback {

        void onSuccess(List<String> results);

        void onFailure(int code, String msg);
    }
}

在上面的例子中,AyncModel类的loadAync()方法里面新建了一个线程来异步加载results字符串列表。如果我们按正常的方式写对应的测试:

public class AyncModelTest extends BaseRoboTestCase {

    @Test
    public void loadAync() throws Exception {
        AyncModel model = new AyncModel();
        final List<String> result = new ArrayList<>();
        model.loadAync(new AyncModel.Callback() {
            @Override
            public void onSuccess(List<String> list) {
                result.addAll(list);
            }

            @Override
            public void onFailure(int code, String msg) {
                fail();
            }
        });
        assertEquals(1, result.size());
    }

}

你会发现上面的测试方法loadAync()永远会fail,这是因为在执行 assertEquals(1, result.size());的时候,loadAync()里面启动的线程压根还没执行完毕呢,因此,callback里面的 result.addAll(list);也没有得到执行,所以result.size()返回永远是0。

Test Aync Fail

前方高能,重点来了,要解决这个问题:如何使用正确的姿势来测试异步代码。通常有两种思路,一是等异步代码执行完了再执行assert断言操作,二是将异步变成同步。接下来,具体讲讲用这两种思路怎样来测试我们的异步代码:

等待异步代码执行完毕

在上面的例子中,我们要做的其实就是是等待Callback里面的代码执行完毕后再执行Asset断言操作。要达到这个目的,大致有两种实现方式:

(1)、使用Thread.sleep
估计大家的第一反应可能和我一样,会使用这种休眠的方式来等待异步代码执行,可能是最简单的方式,这种方式需要设置sleep的时间,所以不可控,建议不适用这种方式。结合上面的例子,具体演示一下:

public class AyncModelTest extends BaseRoboTestCase {

    @Test
    public void loadAync() throws Exception {
        AyncModel model = new AyncModel();
        final List<String> result = new ArrayList<>();
        model.loadAync(new AyncModel.Callback() {
            @Override
            public void onSuccess(List<String> list) {
                result.addAll(list);
            }

            @Override
            public void onFailure(int code, String msg) {
                fail();
            }
        });
        // 使用sleep方式等待异步执行
        Thread.sleep(4000);
        // 此处有坑,如果不加这行代码,就会出现Handler没有执行Runnable的问题
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
        assertEquals(1, result.size());
    }

}

(2)、使用CountDownLatch
有一个非常好用的神器,那就是CountDownLatch。CountDownLatch是一个类,它有两对配套使用的方法,那就是countDown()和await()。await()方法会阻塞当前线程,直到countDown()被调用了一定的次数,这个次数就是在创建这个CountDownLatch对象时,传入的构造参数。结合上面的例子,具体如下:

public class AyncModelTest extends BaseRoboTestCase {

    @Test
    public void loadAync() throws Exception {
        // 使用CountDownLatch
        final CountDownLatch latch = new CountDownLatch(1);
        AyncModel model = new AyncModel();
        final List<String> result = new ArrayList<>();
        model.loadAync(new AyncModel.Callback() {
            @Override
            public void onSuccess(List<String> list) {
                result.addAll(list);
                latch.countDown();
            }

            @Override
            public void onFailure(int code, String msg) {
                fail();
                latch.countDown();
            }
        });
        latch.await(3, TimeUnit.SECONDS);
        // 此处有坑,如果不加这行代码,就会出现Handler没有执行Runnable的问题
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
        assertEquals(1, result.size());
    }

}

使用CountDownLatch来做单元测试,有一个很大的限制,侵入性很高,那就是countDown()必须在测试代码里面写。换句话说,异步操作必需提供Callback,在Callback中执行countDown()方法。如果被测的异步方法(如上面例子的loadAync())不是通过Callback的方式来通知结果,而是通过EventBus来通知外面方法异步运行的结果,那CountDownLatch是无法解决这个异步方法的单元测试问题的。

将异步变成同步

将异步操作变成同步,是解决异步代码测试问题的一种比较直观的思路。这种思路往往比较复杂,根据项目的实际情况来抉择,大致的思想就是将异步操作转换到自己事先准备好的同步线程池来执行。

(1)、通过Executor或ExecutorService方式
如果你的代码是通过Executor或ExecutorService来做异步的,那在测试中把异步变成同步的做法,跟在测试中使用mock对象的方法是一样的,那就是使用依赖注入。在测试代码里面,将同步的Executor注入进去。创建同步的Executor对象很简单,以下就是一个同步的Executor:

Executor immediateExecutor = new Executor() {
    @Override
    public void execute(Runnable command) {
        command.run();
    }
};

(2)、通过New Thread()方式
如果你在代码里面直接通过new Thread()的方式来做异步,这种方式比较简单粗暴,估计你在coding时很爽。但是不幸的告诉你,这样的代码是没有办法变成同步的。那么要做单元测试的话,就需要换成Executor这种方式来做异步操作。还是结合上面的例子,我们来实践一下,修改之后的AyncModel类如下:

public class AyncModel {

    private Handler mUiHandler = new Handler(Looper.getMainLooper());

    private Executor executor;

    public AyncModel(Executor executor) {
        this.executor = executor;
    }

    public void loadAync(final Callback callback) {
        if (executor == null) {
            executor = Executors.newCachedThreadPool();
        }
        executor.execute(new Runnable() {

            @Override
            public void run() {
                final List<String> repos = new ArrayList<>();
                repos.add("test String");
                mUiHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        callback.onSuccess(repos);
                    }
                });
            }
        });
    }

    interface Callback {

        void onSuccess(List<String> results);

        void onFailure(int code, String msg);
    }
}

接着我们看一下修改之后的测试Case:

public class AyncModelTest extends BaseRoboTestCase {

    @Test
    public void loadAync() throws Exception {
        // Executor
        Executor immediateExecutor = new Executor() {
            @Override
            public void execute(Runnable command) {
                command.run();
            }
        };
        AyncModel model = new AyncModel(immediateExecutor);
        final List<String> result = new ArrayList<>();
        model.loadAync(new AyncModel.Callback() {
            @Override
            public void onSuccess(List<String> list) {
                result.addAll(list);
            }

            @Override
            public void onFailure(int code, String msg) {
                fail();
            }
        });
        // 此处有坑,如果不加这行代码,就会出现Handler没有执行Runnable的问题
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
        assertEquals(1, result.size());
    }

}

不知你有没有感觉到,使用Executor方式之后,不管是源代码还是测试代码看起来都很清爽!

(3)、使用AsyncTask
Android提供AsyncTask类,很方便我们进行异步操作,初学Android时,很喜欢这种方式。进行单元测试时,建议使用 AsyncTask.executeOnExecutor(),而不是直接使用AsyncTask.execute(),通过依赖注入的方式,在测试环境下将同步的Executor传进去进去。

(4)、使用RxJava
这个是不得不提的一种方法,鉴于强大的线程切换功能,越来越多的人使用RxJava来做异步操作,RxJava代码的单元测试也是经常被问到的一个问题。不管你是否用到RxJava,反正我现在的项目就用到了。至于如何将异步操作切换到同步执行,之前已经详细讲到了,可以回到上面再看看。

如何Mock网络数据

当我们要对Presenter或者测试UI,考虑到根据网络返回的数据覆盖所有的分支情况,对于一个账号在某一时刻,后端只会返回一种数据结果,这样就限制了做其他情况的单元验证。所以这个时候就需要我们Mock数据来模拟。鉴于项目中使用OKHTTP框架,只要自定义一个Interceptor,在这里进行拦截并Mock你想要的数据,相对来说这种方式比较友好。

OkHttpMockInterceptor类如下:

public class OkHttpMockInterceptor implements Interceptor {

    public OkHttpMockInterceptor() {
    }

    @Override
    public Response intercept(Chain chain) throws IOException {
        Response response = null;
        HttpUrl url = chain.request().url();
        String sym = "";
        String query = url.encodedQuery() == null ? "" : url.encodedQuery();
        if (!query.equals("")) {
            sym = "?";
        }
        String assetPath = url.encodedPath() + sym + query;
        if (JsonStringHelper.isPathExist(assetPath)) {
            response = mock(chain, assetPath);
        }
        if (response == null) {
            response = chain.proceed(chain.request());
        }
        return response;

    }

    private Response mock(Chain chain, String assetPath) {
        if (assetPath == null || "".equals(assetPath)) {
            return null;
        }
        String jsonResult = JsonStringHelper.getMockJsonString(assetPath);
        HttpResponse httpResponse = (HttpResponse) GsonHelper.fromJson(jsonResult, HttpResponse.class);
        return new Response.Builder()
                .code(Integer.valueOf(httpResponse.code))
                .message(httpResponse.msg)
                .request(chain.request())
                .protocol(Protocol.HTTP_1_0)
                .body(ResponseBody.create(MediaType.parse("application/json"), jsonResult))
                .addHeader("content-type", "application/json")
                .build();
    }

}

涉及到的其它类,不是本博客的重点,就不一一列举了。如果项目中不是使用OKHTTP网络框架,而是其他的网络框架如Volley、android-async-http等,还没来得及去探索,感兴趣的同学自己可以深入探索一下。

具体例子

说了这么多,所谓实践是检验真理的唯一标准!下面我们针对具体的例子来实践一把,项目中的onUpdateOrders(OneClickOrderResult result)方法如下:

    public void onUpdateOrders(OneClickOrderResult result) {
        if (result == null || result.orderSource == null || !result.orderSource.equals(orderSource)) {
            return;
        }
        handleUpdateOrders(result);
        if (result.code == 200) {
            if (result.data == null) {
                if (curPage == ORDER_PAGE_INIT) {
                    // case1
                    iView.refreshNewestOrders(null);
                } else if (curPage > ORDER_PAGE_INIT) {
                    // case2
                    iView.refreshMoreOrders(null);
                }
                return;
            }
            // 只展示当前要加载的页码的数据,其他的过滤掉
            if (curPage != result.data.getCurrPage() && result.data.getCurrPage() > 0) {
                return;
            }
            pageCount = result.data.getPageCount();
            if (curPage == ORDER_PAGE_INIT) {
                // case3
                iView.refreshNewestOrders(filterHistoryOrders(result.data.getOrderList()));
            } else if (curPage > ORDER_PAGE_INIT) {
                // case4
                iView.refreshMoreOrders(filterHistoryOrders(result.data.getOrderList()));
            }
            return;
        }
        if (result.code == OneClickFragment.ERROR_ID_MEITUAN_VISIT_OUT_OF_LIMIT && iView.isFragmentVisible()) {
            // case5
            iView.showInputCaptchaDialog();
            return;
        }
        // case6
        iView.refreshError(result.code, curPage > ORDER_PAGE_INIT);
    }

onUpdateOrders()方法是一个没有返回值的公有方法,那么我们该如何下手?首先依赖入参OneClickOrderResult,根据result状态来执行逻辑,其次依赖iView对象。因此,在进行单元测试时,通过mock的方式可以解决这两个数据对象的依赖关系,mock出OneClickOrderResultiView后,其他的就迎刃而解了。分析代码,可以分为6个单元测试Case,如上面的注释,覆盖了onUpdateOrders()方法所有的分支。测试方法如下:

public class OneClickBasePresenterTest extends BaseModelTest {

    @Captor
    private ArgumentCaptor<ArrayList<OneClickOrder.OneClickOrderItem>> captorItems;

    @Test
    public void onUpdateOrdersCase1() throws Exception {
        // mock出IView对象,通过mock隔离外部依赖
        OneClickContract.IView mockView = Mockito.mock(OneClickContract.IView.class);
        // 创建目标类
        OneClickPresenter presenter = new OneClickPresenter(mockView, OneClickOrder.ORDER_SOURCE_BAIDU);

        // 根据Case自己创建数据依赖
        OneClickOrderResult orderResult = new OneClickOrderResult();
        orderResult.orderSource = OneClickOrder.ORDER_SOURCE_BAIDU;
        orderResult.code = 200;
        orderResult.data = null;

        presenter.curPage = OneClickBasePresenter.ORDER_PAGE_INIT;
        // 调用被测方法
        presenter.onUpdateOrders(orderResult);
        // 借助Mockito工具,验证refreshNewestOrders方法是否被调用
        Mockito.verify(mockView).refreshNewestOrders(null);
    }

    @Test
    public void onUpdateOrdersCase2() throws Exception {
        // mock出IView对象,通过mock隔离外部依赖
        OneClickContract.IView mockView = Mockito.mock(OneClickContract.IView.class);
        // 创建目标类
        OneClickPresenter presenter = new OneClickPresenter(mockView, OneClickOrder.ORDER_SOURCE_BAIDU);

        OneClickOrderResult orderResult = new OneClickOrderResult();
        orderResult.orderSource = OneClickOrder.ORDER_SOURCE_BAIDU;
        orderResult.code = 200;
        orderResult.data = null;

        presenter.curPage = OneClickBasePresenter.ORDER_PAGE_INIT + 1;
        // 调用被测方法
        presenter.onUpdateOrders(orderResult);
        // 借助Mockito工具,验证refreshMoreOrders方法是否被调用
        Mockito.verify(mockView).refreshMoreOrders(null);
    }

    @Test
    public void onUpdateOrdersCase3() throws Exception {
        // mock出IView对象,通过mock隔离外部依赖
        OneClickContract.IView mockView = Mockito.mock(OneClickContract.IView.class);
        // 创建目标类
        OneClickPresenter presenter = new OneClickPresenter(mockView, OneClickOrder.ORDER_SOURCE_BAIDU);

        // mock数据依赖OneClickOrderResult
        OneClickOrderResult orderResult = new OneClickOrderResult();
        orderResult.orderSource = OneClickOrder.ORDER_SOURCE_BAIDU;
        presenter.curPage = OneClickBasePresenter.ORDER_PAGE_INIT;
        orderResult.code = 200;

        // mock数据依赖OneClickOrder
        OneClickOrder data = Mockito.mock(OneClickOrder.class);
        Mockito.when(data.getCurrPage()).thenReturn(presenter.curPage);
        Parcel in = Mockito.mock(Parcel.class);
        Mockito.when(in.readString()).thenReturn("1001");
        Mockito.when(in.readInt()).thenReturn(1001);
        OneClickOrder.OneClickOrderItem item = new OneClickOrder.OneClickOrderItem(in);
        ArrayList<OneClickOrder.OneClickOrderItem> items = new ArrayList<>();
        items.add(item);
        items.add(item);
        Mockito.when(data.getOrderList()).thenReturn(items);
        orderResult.data = data;

        presenter.onUpdateOrders(orderResult);
        // 通过ArgumentCaptor来捕获refreshNewestOrders方法被调用时的入参
        Mockito.verify(mockView).refreshNewestOrders(captorItems.capture());
        // 通过Assert断言判断ArgumentCaptor捕获的入参和items数据是否相等
        Assert.assertEquals(captorItems.getValue().size(), items.size());
    }

    @Test
    public void onUpdateOrdersCase4() throws Exception {
        // mock出IView对象,通过mock隔离外部依赖
        OneClickContract.IView mockView = Mockito.mock(OneClickContract.IView.class);
        // 创建目标类
        OneClickPresenter presenter = new OneClickPresenter(mockView, OneClickOrder.ORDER_SOURCE_BAIDU);

        // mock数据依赖OneClickOrderResult
        OneClickOrderResult orderResult = new OneClickOrderResult();
        orderResult.orderSource = OneClickOrder.ORDER_SOURCE_BAIDU;
        orderResult.code = 200;
        presenter.curPage = OneClickBasePresenter.ORDER_PAGE_INIT + 1;

        // mock数据依赖OneClickOrder
        OneClickOrder data = Mockito.mock(OneClickOrder.class);
        Mockito.when(data.getCurrPage()).thenReturn(presenter.curPage);
        Parcel in = Mockito.mock(Parcel.class);
        Mockito.when(in.readString()).thenReturn("1001");
        Mockito.when(in.readInt()).thenReturn(1001);
        OneClickOrder.OneClickOrderItem item = new OneClickOrder.OneClickOrderItem(in);
        ArrayList<OneClickOrder.OneClickOrderItem> items = new ArrayList<>();
        items.add(item);
        items.add(item);
        Mockito.when(data.getOrderList()).thenReturn(items);
        orderResult.data = data;

        // 调用被测方法
        presenter.onUpdateOrders(orderResult);
        // 通过ArgumentCaptor来捕获refreshMoreOrders方法被调用时的入参
        Mockito.verify(mockView).refreshMoreOrders(captorItems.capture());
        Assert.assertEquals(captorItems.getValue().size(), items.size());
    }

    @Test
    public void onUpdateOrdersCase5() throws Exception {
        // mock出IView对象,通过mock隔离外部依赖
        OneClickContract.IView mockView = Mockito.mock(OneClickContract.IView.class);
        // 创建目标类
        OneClickPresenter presenter = new OneClickPresenter(mockView, OneClickOrder.ORDER_SOURCE_BAIDU);

        OneClickOrderResult orderResult = new OneClickOrderResult();
        orderResult.orderSource = OneClickOrder.ORDER_SOURCE_BAIDU;
        orderResult.code = 200;

        orderResult.code = OneClickFragment.ERROR_ID_MEITUAN_VISIT_OUT_OF_LIMIT;
        // 通过mock方式隔离依赖,mockView.isFragmentVisible()返回true
        Mockito.when(mockView.isFragmentVisible()).thenReturn(true);
        // 调用被测方法
        presenter.onUpdateOrders(orderResult);
        // 借助Mockito工具,验证showInputCaptchaDialog方法是否被调用
        Mockito.verify(mockView).showInputCaptchaDialog();
    }

    @Test
    public void onUpdateOrdersCase6() throws Exception {
        // mock出IView对象,通过mock隔离外部依赖
        OneClickContract.IView mockView = Mockito.mock(OneClickContract.IView.class);
        // 创建目标类
        OneClickPresenter presenter = new OneClickPresenter(mockView, OneClickOrder.ORDER_SOURCE_BAIDU);

        OneClickOrderResult orderResult = new OneClickOrderResult();
        orderResult.orderSource = OneClickOrder.ORDER_SOURCE_BAIDU;
        orderResult.code = 500;
        
        // 调用被测方法
        presenter.onUpdateOrders(orderResult);
        // 借助Mockito工具,验证refreshError方法是否被调用
        Mockito.verify(mockView).refreshError(orderResult.code, presenter.isLoadMoreOrders());
    }

}

最后

本博客主要围绕的是Android单元测试中的逻辑测试,自己对单元测试的理解,并结合实际代码讲解。如有不当之处,欢迎指正!下一篇博客将围绕Android单元测试的UI测试。最后,非常感谢您对本篇博客的关注!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容