(可能是)目前最全面的Android Espresso配置指南了

Posted by Piasy on March 13, 2016
本文是 Piasy 原创,发表于 https://blog.piasy.com,请阅读原文支持原创 https://blog.piasy.com/2016/03/13/Android-Espresso-test-start/

安卓开发过程中测试的编写是一个公认的痛点,本文总结了我在AndroidTDDBootStrap工程中配置Espresso测试所遇到的坑,例如神秘报错android.content.res.Resources$NotFoundExceptionjava.util.zip.ZipException: duplicate entry,以及对dagger,mock网络请求的实践,目测应该是目前最全面的指南了 :) 本文涉及的完整代码可以在Github: AndroidTDDBootStrap获取。

配置Gradle依赖

app/build.gradle中加入以下配置:

androidTestCompile project(':testbase')
androidTestCompile (appTestDependencies.androidJUnitRunner) {
    exclude module: 'support-annotations'
}
androidTestCompile (appTestDependencies.mockito) {
    exclude module: 'hamcrest-core'
}
androidTestCompile appTestDependencies.dexmaker
androidTestCompile (appTestDependencies.dexmakerMockito) {
    exclude module: 'hamcrest-core'
    exclude module: 'mockito-core'
}
androidTestCompile (appTestDependencies.androidJUnit4Rules) {
    exclude module: 'support-annotations'
    exclude module: 'hamcrest-core'
}
androidTestApt (appDependencies.daggerCompiler) {
    exclude module: 'dagger'
}

androidTestCompile就是用来声明安卓instrumentation测试的依赖的,它对应的gradle测试命令是./gradlew :app:connectedAndroidTest或者./gradlew :app:cAT。而androidTestApt则是在instrumentation测试代码中使用apt插件生成代码的(dagger2用到)。

在这里我还遇到了两个特别诡异的错误,第一个是运行测例直接失败,logcat报错如下:

android.content.res.Resources$NotFoundException: Resource ID

Google几番也没有找到什么头绪,最后从StackOverflow上一个回答的评论中找到了原因:归根结底还是因为发生了重复依赖!通过./gradlew :app:dependencies分析app的依赖,发现testbase这个module依赖了appcompat-v7,而app则直接编译依赖了它,两者在instrumentation测试中发生了冲突,导致了这个诡异的闪退,移除testbaseappcompat-v7的依赖之后就解决了这个问题。

第二个是执行./gradlew :model:cAT失败,gradle报错如下:

java.util.zip.ZipException: duplicate entry:      javax/annotation/Generated.class

同样是几番Google无果,反复折腾了几天,无奈只能自行分析。从报错来看还是发生了依赖重复,存在两个javax/annotation/Generated.class类,通过./gradlew :model:dependencies分析model的依赖,最终发现testbase这个module通过espresso间接依赖了javax.annotation-api,而model则通过base编译依赖了javax.annotation,两者发生了重复,导致了这个问题。通过配置testbase,在依赖espresso的时候exclude module: 'javax.annotation-api'终于解决了这个问题。

依赖注入

如何为测试代码配置依赖注入这个问题已经纠缠我两年了。

最初听从了某大神的博客建议,debug和release两个variant用作不同用途,debug一套DI的代码,专门用于测试,release一套DI的代码,专门用于非测试(博客出处已经不可考了)。这种方式最大的问题就是需要维护两套代码,而且他们无法同时被IDE展示(必须通过build variant切换),在重构的时候无法同时重构,经常导致重构了业务代码,结果测试代码没有被重构,必须手动修改,非常蛋疼。

后来参考了另一位大神Chiu-Ki的博客,采取了Application类暴露接口设置dagger component的方式,设置进去的component负责提供mock的对象,并且它可以把依赖注入到测试代码中。这种方式比较简洁,但是Application类却暴露了不应该暴露的接口,不是十分优雅。

最终Chiu-Ki再次发力,通过编写一个MockApplication类,并通过自定义Test Runner来启动mock application,完美解决了这一问题。mock application类继承自application类,在其中初始化component为提供mock的component,而mock component又把依赖注入到测试代码中,完美 :)

但是dagger在涉及到继承的时候有一个细节需要注意:如果component定义的inject接口接受的是父类型,那么当子类型实例调用inject(this)时,子类型中需要注入的依赖(@Inject注解的成员)将无法注入!需要编写一个component的子类,把inject接口的参数类型声明为子类型,并且声称component子类的实例进行依赖注入。这种情况下父类中需要注入的依赖是可以成功注入的,因为dagger可以搜索父类并把依赖注入到父类中,但反过来是行不通的,dagger是无法搜索子类并注入依赖到子类中的(因为编译期间可能根本就无法获取到子类的符号呀,怎么能搜索子类呢?)。

Mock网络请求

可以说目前最流行的网络请求库就是OkHttp了,而且从安卓6.0开始它就是系统的默认实现了。而OkHttp还提供了另一个无比强大的工具:MockWebServer。而OkHttp + Retrofit应该是REST API请求的标配了,下面我就总结一下如何利用MockWebServer来进行网络请求mock,对APP进行集成测试。

Retrofit 2.0中没有了end point的概念,取而代之的是base url,在创建Retrofit对象的时候可以配置,在测试中我们配置base url为http://localhost:9876/,同时在测试用例中我们配置MockWebServer运行在9876端口。这样任何通过Retrofit发起的API调用都是请求的MockWebServer了。

MockWebServer可以设置Dispatcher,可以根据不同的请求返回不同的数据,在本工程的一个测例中,dispatcher的设置如下:

new Dispatcher() {
    @Override
    public MockResponse dispatch(final RecordedRequest request)
            throws InterruptedException {
        final String path = request.getPath();
        if (path.startsWith("/search/users?")) {
            return new MockResponse().setBody(
                    MockProvider.provideSimplifiedGithubUserSearchResultStr());
        } else {
            return new MockResponse();
        }
    }
};

如此通过Retrofit调用/search/users接口返回的就是mock的数据了。完美 :)