测试异步方法
深切体会到,测试异步方法,是整个单元测试的难点和重点,为什么这么说呢?问题很明显,当测试方法跑完了的时候,被测的异步代码可能还在执行没跑完,这就有问题了。再者就是实现异步操作的框架比较多样。下面有这么一个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。
前方高能,重点来了,要解决这个问题:如何使用正确的姿势来测试异步代码。通常有两种思路,一是等异步代码执行完了再执行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,反正我现在的项目就用到了。至于如何将异步操作切换到同步执行,之前已经详细讲到了,可以回到上面再看看。