前言
在我的上一篇文章 通关Android 单元测试(一)Espresso篇:简介&基础使用 中,简单阐述了Espresso的基本使用,以及为什么我们要使用Espresso。
实际上,Espresso进行一个简单的同步功能测试并不难,比如我们点击了一个Button,点击后改变对应某个TextView的内容,这很简单。但实际正常开发中,这种简单的逻辑测试是很少见的,相反,我们需要测试的是各种各样的异步测试,比如:
情景一:点击进入Activity,网络请求数据加载,成功后数据展示在界面上。
情景二:点击进入Activity,获得缓存,网络请求数据加载,成功后数据展示在界面上,处理缓存。
情景N : ……
假设这样一个简单的网络请求测试:
@Test
public void testHttp() {
//假设我们请求网络数据,如果请求网络数据成功,让TextView显示"网络请求成功"
//同时ImageView从不可见变为可见。
//如果我们直接检查是不是请求到了数据
onView(withId(R.id.textView)).check(matches(withText("网络请求成功!")));
onView(withText(R.id.imageView)).check(matches(isDisplayed()));
}
如果我们直接测试,那么很大概率会报错,因为在我们要测试数据是否展示在UI上时,网络数据很有可能还没有获取到。
显然这很难处理,因为我们不知道数据到底什么时候才能获取到,有同学抖了个机灵,说我们可以这样:
@Test
public void testHttp() {
// 我们一进来就先让他等待5秒,等数据加载完毕再检查UI
Thread.sleep(5000);
// 5秒结束,我们检查是不是请求到了数据
onView(withId(R.id.textView)).check(matches(withText("网络请求成功!")));
onView(withText(R.id.imageView)).check(matches(isDisplayed()));
}
这样可以实现吗,确实很有可能通过,但是这种测试显然属于饮鸩止渴,因为网络情况是在不断变化的,也许0.5s就能获取网络数据,也有可能数十秒后才能获取,这样前者导致我们浪费了4.5s的时间,后者在网络状态属于正常的时候测试结果失败,这都是我们不愿看到的结果。
我们更希望在获取到网络数据之后,立即进行下一步的测试,如果网络请求需要0.5s,我们就等待0.5s之后测试UI,如果需要数十秒,我们就等数十秒。
今天我们结合谷歌官方的MVP模式下的todoApp demo,通过demo,我们学习和了解一下如何进行异步测试。
一、依赖及API
我们在Module的build.gradle文件中添加依赖:
final ESPRESSO_VERSION = '2.2.2'
dependencies {
...
...
//Espresso依赖,AndroidStudio 2.2+之后自动依赖
androidTestCompile("com.android.support.test.espresso:espresso-core:${ESPRESSO_VERSION}", {
exclude group: 'com.android.support', module: 'support-annotations'
})
//Espresso的IdlingResource异步接口依赖:
compile("com.android.support.test.espresso:espresso-idling-resource:${ESPRESSO_VERSION}") {
exclude module: 'support-annotations'
}
androidTestCompile("com.android.support.test.espresso:espresso-idling-resource:${ESPRESSO_VERSION}") {
exclude module: 'support-annotations'
}
...
...
}
我们想要对异步代码进行单元测试,首先要了解IdlingResource这个接口,接口需要我们实现的方法如下:
public interface IdlingResource {
/**
* 用来标识 IdlingResource 名称
*/
public String getName();
/**
* 当前 IdlingResource 是否空闲 .
*/
public boolean isIdleNow();
/**
注册一个空闲状态变换的ResourceCallback回调
*/
public void registerIdleTransitionCallback(ResourceCallback callback);
/**
* 通知Espresso当前IdlingResource状态变换为空闲的回调接口
*/
public interface ResourceCallback {
/**
* 当前状态转变为空闲时,调用该方法告诉Espresso
*/
public void onTransitionToIdle();
}
}
对于这个接口,当初在网上看了不少的用法,也尝试用了不少,感觉都不太好用,感觉特别麻烦!原因很简单,我们一定要每次都定义这样一个接口吗?
后来花了点时间认真翻了翻Google的todoapp关于单元测试的处理,豁然开朗,毕竟是谷歌,代码思路非常清晰。
我们来看一看Google的工程师们是如何进行测试的:
二、Google代码:
1、首先自定义一个SimpleCountingIdlingResource
这个IdlingResource实现了IdlingResource接口:
public final class SimpleCountingIdlingResource implements IdlingResource {
private final String mResourceName;
//这个counter值就像一个标记,默认为0
private final AtomicInteger counter = new AtomicInteger(0);
private volatile ResourceCallback resourceCallback;
public SimpleCountingIdlingResource(String resourceName) {
mResourceName = resourceName;
}
@Override
public String getName() {
return mResourceName;
}
@Override
public boolean isIdleNow() {
return counter.get() == 0;
}
@Override
public void registerIdleTransitionCallback(ResourceCallback resourceCallback) {
this.resourceCallback = resourceCallback;
}
//每当我们开始异步请求,把counter值+1
public void increment() {
counter.getAndIncrement();
}
//当我们获取到网络数据后,counter值-1;
public void decrement() {
int counterVal = counter.decrementAndGet();
//如果这时counter == 0,说明异步结束,执行回调。
if (counterVal == 0) {
//
if (null != resourceCallback) {
resourceCallback.onTransitionToIdle();
}
}
if (counterVal < 0) {
//如果小于0,抛出异常
throw new IllegalArgumentException("Counter has been corrupted!");
}
}
}
2、定义一个该IdlingResource的管理类EspressoIdlingResource:
我们实际上并不直接处理SimpleCountingIdlingResource,而是将其业务处理交给管理类去处理。
public class EspressoIdlingResource {
private static final String RESOURCE = "GLOBAL";
private static SimpleCountingIdlingResource mCountingIdlingResource =
new SimpleCountingIdlingResource(RESOURCE);
public static void increment() {
mCountingIdlingResource.increment();
}
public static void decrement() {
mCountingIdlingResource.decrement();
}
public static IdlingResource getIdlingResource() {
return mCountingIdlingResource;
}
}
3、Activity中如何使用:
在Activity中我们先添加这样一个方法,这个方法是给测试时提供获取IdlingResource的接口:
@VisibleForTesting
public IdlingResource getCountingIdlingResource() {
return EspressoIdlingResource.getIdlingResource();
}
当我们开始异步请求前,将IdlingResource的标记加一,这就意味着已经开始异步了:
//在开始异步请求前添加这行代码,意味着开始了异步
EspressoIdlingResource.increment();
当请求结束,我们再添加这行代码,说明网络请求结束:
//图片加载成功,结束异步
if (!EspressoIdlingResource.getIdlingResource().isIdleNow()) {
EspressoIdlingResource.decrement();
}
好的,我们已经在我们的代码中将需要做到的都做好了,接下来就是去AndroidTest中写我们的测试代码了!
4、测试代码:
我们直接回到文章开始时的那个小案例:
public class A06AsyncActivity2Test {
@Rule
public ActivityTestRule<A06AsyncActivity2> activityRule = new ActivityTestRule<>(A06AsyncActivity2.class);
private IdlingResource idlingresource;
@Before
public void setUp() throws Exception {
//调用Activity中我们已经设置好的getIdlingresource()方法,获取Idlingresource对象
idlingresource = activityRule.getActivity().getIdlingresource();
//去掉下行注释,只有异步结束后,才进行接下来的测试代码(tests passed)
//注册异步监听,当该idlingresource中的counter标记值为0时才进行接下来的测试代码
Espresso.registerIdlingResources(idlingresource);
}
@Test
public void onLoadingFinished() throws Exception {
// 不再需要这样的代码
// Thread.sleep(5000);
// 未注册idlingResource时,立即进行test,此时异步并未结束,报错(tests failed)
onView(withId(R.id.text))
.check(matches(withText("success!")));
}
@After
public void release() throws Exception {
//我们在测试结束后取消注册,释放资源
Espresso.unregisterIdlingResources(idlingresource);
}
}
Espresso.registerIdlingResources(idlingresource);
我们将上面这行代码分别注释/打开,进行测试,发现在没有注册IdlingResources时,进行测试会报错,当注册后,代码执行逻辑应该是:
1.Activity中开始异步请求(IdResource.counter从0 -> 1),
2.异步请求中…(这时监听到的counter一直为1)
3.异步请求结束(这时监听到的counter 1 -> 0)
4.执行IdlingResources中执行 resourceCallback.onTransitionToIdle();
5.测试代码继续进行测试
也就是说
Espresso.registerIdlingResources(idlingresource)
这行代码和之前的
Thread.sleep(5000);
这两行代码都是一样的效果,阻塞住当前的测试,只不过前者更优越的是能够在异步结束之后马上执行接下来的测试代码,从效果上来说不知好了多少。
三、OkHttp异步请求测试:
其实之前的SimpleCountingIdlingResource接口已经能够满足我们日常开发中的测试需求了,但是如果您所使用的网络请求库刚好是OkHttp,那么这里有一个更简单的实现方式,先看代码:
依赖:
androidTestCompile('com.jakewharton.espresso:okhttp3-idling-resource:1.0.0') {
exclude module: 'support-annotations'
exclude module: 'okhttp'
}
测试代码中使用:
@RunWith(AndroidJUnit4.class)
public class A07OkhttpActivityTest {
@Rule
public ActivityTestRule<A07OkhttpActivity> rule = new ActivityTestRule<>(A07OkhttpActivity.class);
@Test
public void requestHttpTest() throws Exception {
//初始化
OkHttp3IdlingResource idlingResource = OkHttp3IdlingResource.create("okhttp", OkHttpProvider.getOkHttpInstance());
//注册
Espresso.registerIdlingResources(idlingResource);
onView(withId(R.id.tv_name))
.check(matches(withText("qingmei2")));
//解除注册
Espresso.unregisterIdlingResources(idlingResource);
}
}
OkHttpProvider是提供OkHttpClient对象的类:
public abstract class OkHttpProvider {
private static OkHttpClient instance = null;
public static OkHttpClient getOkHttpInstance() {
if (instance == null) {
HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor()
.setLevel(HttpLoggingInterceptor.Level.BODY);
instance = new OkHttpClient()
.newBuilder()
.addInterceptor(interceptor)
.build();
}
return instance;
}
}
也就是说,我们并不需要去自己实现IdlingResource接口,已经有人帮我们封装好了,我们直接拿去测试就好了。
有同学也许会说,这是谁封装的,靠谱不啊。
作者就是GitHub上著名的开源狂魔 JakeWharton大神,关于他「开源」以及「参与开源贡献大量代码」的项目有:
https://github.com/JakeWharton/butterknife
https://github.com/JakeWharton/RxBinding
https://github.com/square/retrofit
https://github.com/ReactiveX/RxAndroid
所以说放心去用,绝对靠谱。
不过经笔者尝试,发现这个库并不能直接配合Rx+Retrofit使用,略遗憾。
总结
学会了异步测试,接下来就可以尝试去写测试代码了。
推荐一些好的文章,特别感谢,给我单元测试的学习提供了很大的帮助,再次真挚感谢!
本系列的所有代码都已托管GitHub:
本文案例代码所在位置:
下一篇文章,我们将对Espresso的其他高阶用法进行讲解,敬请关注。