由于工作工作需要,对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
- Retrofit api 测试
这个如果后续项目需要将API 切换为 REST 方式,可以参考如下测试方案(先做个留存)
https://segmentfault.com/a/1190000000407190
https://github.com/swanson/retrofit-demo
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