Android Robolectric 测试框架探究

由于工作工作需要,对Android的测试框架做了个初步的研究,这里记录下,也会记录若干参考资料和例子,方便自己以后回顾。本文主要记录了Robolectric框架的探究过程。

1 简介

通过实现一套JVM能运行的Android代码,然后在unit test运行的时候去截取android相关的代码调用,然后转到他们的他们实现的代码去执行这个调用的过程。举个例子说明一下,比如android里面有个类叫TextView,他们实现了一个类叫ShadowTextView。这个类基本上实现了TextView的所有公共接口,假设你在unit test里面写到String text = textView.getText().toString();。在这个unit test运行的时候,Robolectric会自动判断你调用了Android相关的代码textView.getText(),然后这个调用过程在底层截取了,转到ShadowTextView的getText实现。而ShadowTextView是真正实现了getText这个方法的,所以这个过程便可以正常执行。

除了实现Android里面的类的现有接口,Robolectric还做了另外一件事情,极大地方便了unit testing的工作。那就是他们给每个Shadow类额外增加了很多接口,可以读取对应的Android类的一些状态。比如我们知道ImageView有一个方法叫setImageResource(resourceId),然而并没有一个对应的getter方法叫getImageResourceId(),这样你是没有办法测试这个ImageView是不是显示了你想要的image。而在Robolectric实现的对应的ShadowImageView里面,则提供了getImageResourceId()这个接口。你可以用来测试它是不是正确的显示了你想要的Image.

对于一些测试对象依赖度较高而需要解除依赖的场景,我们可以借助Mock框架。
另外在Robolectric 3.0 中,增加了对真实网络请求的支持,可以进行部分网络请求的测试。

简单的说,这就是一个让你,不用启动虚拟机,或者安装App, 通过编译器就可以测试代码功能的框架。

2 配置

Android单元测试依旧需要JUnit框架的支持,Robolectric只是提供了Android代码的运行环境。如果使用Robolectric 3.0,依赖配置如下:

dependencies {
    ……
    testCompile 'junit:junit:4.12'
    testCompile ‘org.robolectric:robolectric:3.0'
}

建议使用3.0版本,低于3.0的版本,还需要配置若干内容,容易遗漏。
还需要将Android Studio 中 Build Variants 中(一般在编辑界面左侧) Test Artifact 选为 Unit Tests (如果不选的话,在写测试用例时,会有大量报错提示)
如果编辑界面中没有 Build Variants ,可以通过 View -> Tool Windows -> Build Variants 找到。

这里写图片描述

3 使用

3.1 Helloword — 以IMCache为例。

这是项目中一个简单的缓存类。
IMCache 中方法是和缓存相关的,有比较明确的输入和输出,比较适合做单元测试。
首先,创建测试类的框架。如下图所示:

这里写图片描述

然后点击 Create new test 进入选项界面。

这里写图片描述

选择 JUnit4。Generate 选项中,可以把 setUp 和 tearDown 都勾选(后续如不需实现,可直接留着空方法)
Member列表中是该类中所有 public 方法,可全部勾选。
点击ok 后,会在当前工程src/test/java + IMCache 的包名组成的路径下,生成一个IMCacheTest 文件。

这里写图片描述

打开此文件后需要在类前面加上如下的注解
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21)

这里写图片描述

@RunWith(RobolectricTestRunner.class) 这个注解,如果测试类中 Activity 等 Android 组件相关,需要使用RobolectricGradleTestRunner.class

测试用例结构均已生成好,以 test + 原来的 public 方法名命名。

    @Test
    public void testGetLastMessageID() throws Exception {
        mDefaultCache = PreferenceManager.getDefaultSharedPreferences(mApplication);
        int result = IMCache.getInstance(mApplication).getLastMessageID();
        assertEquals(result, 0);
    }

运行测试用例。运行完毕后可以去控制台查看结果。

这里写图片描述

3.2 一些基本用法汇总

(后续会细化下,目前先用一个Github比较不错的项目。)
https://github.com/geniusmart/LoveUT
官方示例
https://github.com/robolectric/robolectric-samples

3.3 网络请求相关的测试方法

汇总了一些常见场景和常见网络请求框架的简单测试方法,其他框架可以参考,思路应该可以迁移过去。

  • http请求测试
    基础的http请求相关的测试方法,Robolectric 3.0 已经支持。
    不过此种场景,在实际编码中,应该使用较少,项目代码中,大部分场景实用的封装的网络请求框架。
    如果利用Robolectric进行测试,可使用测试框架自带的一个 FakeHttp,从名字上看,就是一个模拟的http请求,不过此请求也可以真实请求网络数据。
//  下面这句是设置是否拦截真实的请求, 如果不设置这行代码,默认为true
//  如果设置为false,http则会真实访问网络
FakeHttp.getFakeHttpLayer().interceptHttpRequests(true);
//  模拟一个返回的例子
ProtocolVersion httpProtocolVersion =new ProtocolVersion("HTTP",1,1);
HttpResponse httpResponse = new BasicHttpResponse(httpProtocolVersion, 400, “OK");
//  设置一个默认返回,如果设置了, 所有请求返回都是这个
FakeHttp.setDefaultHttpResponse(httpResponse);
//  添加一个返回规则,制定一个请求的期望返回
FakeHttp.addHttpResponseRule("http://www.baidu.com", httpResponse);
//  执行请求
HttpGet httpGet = new HttpGet("http://www.baidu.com");
HttpResponse resultResponse = new DefaultHttpClient().execute(httpGet);
//  断言比较结果
assertThat(resultResponse, is(httpResponse));

这里还有一个谷歌官方的例子可以参考,使用此框架来进行原生的http请求测试
DefaultRequestDirectorTest

  • okhttp 异步请求测试
    异步请求由于存在异步的过程为,断言在进行比较时,还未拿到请求的返回结果,影响比较结果。
    为了解决异步返回有时间差的问题,使用 CountDownLatch 对线程进行处理使用此方式后,能解决此问题,可以测试真实的网络请求。并得到真实结果。
final CountDownLatch latch = new CountDownLatch(1);
mOkHttpRequestClient.request(api, new JsonResultCallback() {
    @Override
    public void onResponse(Object response, int stateCode) {
        mStateCode = stateCode;
        mResponse = (JsonObject) response;
    }
    @Override
    public void onAfter() {
        super.onAfter();
        latch.countDown();
    }
}, false);

latch.await();
assertEquals(mStateCode, 200);
assertNotNull(mResponse);
assertNotNull(mResponse.get("error_response"));
  • volley 异步请求测试
    volley 也是一种常见的请求框架,在单元过程中,也存在和 okhttp 类似的问题,也是采用同样的方式解决,不过由于 volley 本身和 okhttp 实现有些区别,所有有些需要 volley 特别处理的地方。
    okhttp 通过 OkHttpRequestClient 发起请求,OkHttpRequestClient 通过mock的方式虚拟一个,
    而 volley 中 需要将请求添加到 RequestQueue 中执行后,请求会执行 RequestQueue 通过 volley 自带方式拿到的,再实现时,存在问题。
    可能的原因:由于Volley接收到请求结果后,会将onResponse和onErrorResponse放在UIThread上运行,而Robolectric对UIThread模拟调用好像有问题,因此这里需要另外建立一个RequestQueue用新的responseDelivery代替原来的对UIThread的调用
    找到一种解决方案如下:
private RequestQueue getTestQequestQueue(Context context){
        HttpStack stack = new HurlStack();
//        HttpStack stack = new HttpClientStack(new DefaultHttpClient());
        Network network = new BasicNetwork(stack);
        ResponseDelivery responseDelivery = new ExecutorDelivery(Executors.newSingleThreadExecutor());
        RequestQueue queue = new RequestQueue(new NoCache(), network, 4, responseDelivery);
        queue.start();
        return queue;
    }

完整的一个测试用例如下:

完整的一个测试用例如下

http://apistore.baidu.com/apiworks/servicedetail/794.html  测试一个查找手机归属地请求
@Before
    public void setUp() throws Exception {
        errNum = 0;
        mApplication = MyApplication.getAppContext();
        mRequestQueue = getTestQequestQueue(mApplication);
        //FakeHttp.getFakeHttpLayer().interceptHttpRequests(false);
    }
    @After
    public void tearDown() throws Exception {
    }
    @Test
    public void testWeatherInfo1() throws Exception {
        final CountDownLatch latch = new CountDownLatch(1);
        //故意填写错误参数,是返回值为 -1
        BaseApi api = new PhoneNumApi().weatherInfo("");
        mRequestQueue.add(new BaseJsonObjectRequest(
                mApplication,
                api,
                new Response.Listener<JSONObject>() {
                    @Override
                    public void onResponse(JSONObject jsonObject) {
                        try {
                            errNum = jsonObject.getInt("errNum");
                            latch.countDown();
                        } catch (JSONException e) {
                            e.printStackTrace();
                        }
                    }
                },
                new Response.ErrorListener() {
                    @Override
                    public void onErrorResponse(VolleyError volleyError) {
                        latch.countDown();
                    }
                }
        ));
        //  设置CountDownLatch 超时的时间
        latch.await(30, TimeUnit.SECONDS);
        assertEquals(errNum, -1);
    }

完整示例路径:
https://github.com/weijianfeng/AndroidTest/tree/master/RobolectricVolley

4 若干注意点

4.1 测试中使用上下文

针对Android 单元测试,有时有需要使用上下文的场景。
可以通过如下方式获取。

private Application mApplication;
mApplication = RuntimeEnvironment.application;

4.2 Androidhttpclient 报错

测试用例的代码中,如果涉及到网络请求,有时会报 Androidhttpclient 的相关错误。一个解决方案是在gradle文件中做如下修改

android {
    ……
    useLibrary 'org.apache.http.legacy'
} 

4.3 关于 RobolectricTestRunner 注解的问题

在大部分场景下,在测试类前的注解声明,建议还是使用 RobolectricGradleTestRunner。
RobolectricTestRunner 与 RobolectricGradleTestRunner 的区别,并没有完全搞清楚。
不过有一个区别基本明确。
RobolectricTestRunner注解时,RuntimeEnvironment.application 拿到是的一个原生的application.
而RobolectricGradleTestRunner 拿到的,是manifest中定义的那个application
如果项目中专门定义过application,并在manifest 中声明。 那么使用上面两个注解的变现形式可能会不同。
(这也是为什么 3.1 例子中使用的是RobolectricTestRunner,因为manifest中定义的那个application中有绑定服务操作,框架不支持)

4.4 低版本Robolectric 构建问题

如果有引入低于3.0版本,如果只按照 2 配置 中的步骤进行配置,可能会失败。
需要增加如下配置

classpath 'org.robolectric:robolectric-gradle-plugin:0.14.+'//这行配置在buildscript的dependencies中
apply plugin: 'robolectric'
androidTestCompile 'org.robolectric:robolectric:2.4'

4.5 绑定服务问题

Robolectric 对 Service 组件支持有限,只支持一些简单用法,对于绑定服务,当前是不支持的。
来自官方的建议是,使用 Espresso 框架。
Robolectric, by design, does not try and emulate service binding functionality. What you should do depends on what you’re trying to test. You can use @SuperJugy’s work-around if you just need to get past the NPE. If you are actually trying to test activity-service interaction, you test it with an integration test (e.g. Espresso).
https://github.com/robolectric/robolectric/issues/834

4.6 “Method … not mocked.”

在运行代码时,经常出现 method … not mocked 的问题,主要在调用一些系统方法时候会抱错,在gradle文件中增加如下配置项即可。

android {
  // ...
  testOptions { 
    unitTests.returnDefaultValues = true
  }
}

官方详细解释:http://tools.android.com/tech-docs/unit-testing-support#TOC-Method-…-not-mocked.-

5 与CI, Jenkins 结合进行代码保障

由于Robolectric 写出单元测试代码,不需要真实android环境就可以进行测试,所以较为适合提交代码时,进行简单的功能验证和持续集成。
如下图所示,在jeckins 的配置,默认的build,已经包含了出发 单元测试 test 目录测试套的构建。
gradle 配置可以如图所示,选择 wrapper 的版本,也可以 invoke 指定版本(可能存在版本匹配问题导致构建失败)。
如果测试套有问题,或遇到环境配置问题,导致测试用例一直执行不成功的情况,可以在 Tasks 的配置中,build 下一行加上 -x test,去掉运行测试用例的构建过程即可。
这里写图片描述

6 参考资料

官方资料: https://github.com/robolectric/
美团技术团队资料: http://tech.meituan.com/Android_unit_test.html

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值