异步无处不在,特别是网络请求,必须在子线程中执行。异步一般用来处理比较耗时的操作,除了网络请求外还有数据库操作、文件读写等等。一个典型的异步方法如下:
public class DataManager {
public interface O...
异步无处不在,特别是网络请求,必须在子线程中执行。异步一般用来处理比较耗时的操作,除了网络请求外还有数据库操作、文件读写等等。一个典型的异步方法如下:
public class DataManager {
public interface OnDataListener {
public void onSuccess(List<String> dataList);
public void onFail();
public void loadData(final OnDataListener listener) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
List<String> dataList = new ArrayList<String>();
dataList.add("11");
dataList.add("22");
dataList.add("33");
if(listener != null) {
listener.onSuccess(dataList);
} catch (InterruptedException e) {
e.printStackTrace();
if(listener != null) {
listener.onFail();
}).start();
上面代码里开启了一个异步线程,等待1秒之后在回调函数里成功返回数据。通常情况下,我们针对loadData()方法写如下单元测试:
@Test
public void testGetData() {
final List<String> list = new ArrayList<String>();
DataManager dataManager = new DataManager();
dataManager.loadData(new DataManager.OnDataListener() {
@Override
public void onSuccess(List<String> dataList) {
if(dataList != null) {
list.addAll(dataList);
@Override
public void onFail() {
Assert.assertEquals(3, list.size());
执行这段测试代码,你会发现永远都不会通过。因为loadData()
是一个异步方法,当我们在执行Assert.assertEquals()
方法时,loadData()
异步方法里的代码还没执行,所以list.size()
返回永远是0。
这只是一个最简单的例子,我们代码里肯定充斥着各种各样的异步代码,那么对于这些异步该怎么测试呢?
要解决这个问题,主要有2个思路:一是等待异步操作完成,然后在进行assert
断言;二是将异步操作变成同步操作。
1. 等待异步完成:使用CountDownLatch
前面的例子,等待异步完成实际上就是等待callback函数执行完毕,使用CountDownLatch可以达到这个目标,不熟悉该类的可自行搜索学习。修改原来的测试用例代码如下:
@Test
public void testGetData() {
final List<String> list = new ArrayList<String>();
DataManager dataManager = new DataManager();
final CountDownLatch latch = new CountDownLatch(1);
dataManager.loadData(new DataManager.OnDataListener() {
@Override
public void onSuccess(List<String> dataList) {
if(dataList != null) {
list.addAll(dataList);
//callback方法执行完毕侯,唤醒测试方法执行线程
latch.countDown();
@Override
public void onFail() {
try {
//测试方法线程会在这里暂停, 直到loadData()方法执行完毕, 才会被唤醒继续执行
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
Assert.assertEquals(3, list.size());
CountDownLatch适用场景:
1.方法里有callback函数调用的异步方法,如前面所介绍的这个例子。
2.RxJava实现的异步,RxJava里的subscribe方法实际上与callback类似,所以同样适用。
CountDownLatch同样有它的局限性,就是必须能够在测试代码里调用countDown()
方法,这就要求被测的异步方法必须有类似callback的调用,也就是说异步方法的调用结果必须是通过callback调用通知出去的,如果我们采用其他通知方式,例如EventBus、Broadcast将结果通知出去,CountDownLatch则不能实现这种异步方法的测试了。
实际上,可以使用synchronized
的wait/notify
机制实现同样的功能。我们将测试代码稍微改改如下:
@Test
public void testGetData() {
final List<String> list = new ArrayList<String>();
DataManager dataManager = new DataManager();
final Object lock = new Object();
dataManager.loadData(new DataManager.OnDataListener() {
@Override
public void onSuccess(List<String> dataList) {
if(dataList != null) {
list.addAll(dataList);
synchronized (lock) {
lock.notify();
@Override
public void onFail() {
try {
synchronized (lock) {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
Assert.assertEquals(3, list.size());
CountDownLatch与wait/notify相比而言,语义更简单,使用起来方便很多。
2. 将异步变成同步
下面介绍几种不同的异步实现。
2.1 使用RxJava
RxJava现在已经被广泛运用于Android开发中了,特别是结合了Rotrofit框架之后,简直是异步网络请求的神器。RxJava发展到现在最新的版本是RxJava2,相比RxJava1做了很多改进,这里我们直接采用RxJava2来讲述,RxJava1与之类似。对于前面的异步请求,我们采用RxJava2来改造之后,代码如下:
public Observable<List<String>> loadData() {
return Observable.create(new ObservableOnSubscribe<List<String>>() {
@Override
public void subscribe(ObservableEmitter<List<String>> e) throws Exception {
Thread.sleep(1000);
List<String> dataList = new ArrayList<String>();
dataList.add("11");
dataList.add("22");
dataList.add("33");
e.onNext(dataList);
e.onComplete();
}).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
RxJava2都是通过subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
来实现异步的,这段代码表示所有操作都在IO线程里执行,最后的结果是在主线程实现回调的。这里要将异步变成同步的关键是改变subscribeOn()的执行线程,有2种方式可以实现:
将subscribeOn()以及observeOn()的参数通过依赖注入的方式注入进来,正常运行时跑在IO线程中,测试时跑在测试方法运行所在的线程中,这样就实现了异步变同步。
使用RxJava2提供的RxJavaPlugins工具类,让Schedulers.io()
返回当前测试方法运行所在的线程。
@Before
public void setup() {
RxJavaPlugins.reset();
//设置Schedulers.io()返回的线程
RxJavaPlugins.setIoSchedulerHandler(new Function<Scheduler, Scheduler>() {
@Override
public Scheduler apply(Scheduler scheduler) throws Exception {
//返回当前的工作线程,这样测试方法与之都是运行在同一个线程了,从而实现异步变同步。
return Schedulers.trampoline();
@Test
public void testGetDataAsync() {
final List<String> list = new ArrayList<String>();
DataManager dataManager = new DataManager();
dataManager.loadData().subscribe(new Consumer<List<String>>() {
@Override
public void accept(List<String> dataList) throws Exception {
if(dataList != null) {
list.addAll(dataList);
}, new Consumer<Throwable>() {
@Override
public void accept(Throwable throwable) throws Exception {
Assert.assertEquals(3, list.size());
2.2 new Thread()方式做异步操作
如果你的代码里还有直接new Thread()实现异步的方式,唯一的建议是赶紧去使用其他的异步框架吧。
2.3 使用Executor
如果我们使用Executor来实现异步,可以使用依赖注入的方式,在测试环境中将一个同步的Executor注入进去。实现一个同步的Executor很简单。
Executor executor = new Executor() {
@Override
public void execute(Runnable command) {
command.run();
2.4 AsyncTask
现在已经不推荐使用AsyncTask
了,如果一定要使用,建议使用AsyncTask.executeOnExecutor(Executor exec, Params... params)
方法,然后通过依赖注入的方式,在测试环境中将同步的Executor
注入进去。
本文主要介绍了针对异步代码进行单元测试的2种方法:一是等待异步完成,二是将异步变成同步。前者需要写很多侵入性代码,通过加锁等机制来实现,并且必须符合callback机制。其他还有很多实现异步的方式,例如IntentService、HandlerThread、Loader等,综合比较下来,使用RxJava2来实现异步是一个不错的方案,它不仅功能强大,并且在单元测试中能毫无侵入性的将异步变成同步,在这里强烈推荐!
系列文章:
Android单元测试(一):前言
Android单元测试(二):什么是单元测试
Android单元测试(三):测试难点及方案选择
Android单元测试(四):JUnit介绍
Android单元测试(五):JUnit进阶
Android单元测试(六):Mockito学习
Android单元测试(七):Robolectric介绍
Android单元测试(八):怎样测试异步代码
Android异步消息处理机制之Handler、Looper、Message
因为Android UI线程是线程不安全的,在子线程中更新UI会直接程序崩溃,另外当UI线程需要执行一个比较耗时的操作的话(IO操作,网络通信等),若是执行时间超过5s,程序会直接ANR,为了解决上述问题,可以使用异步消息处理机制[Handler]
【Android 异步操作】Handler 机制 ( MessageQueue 消息队列的阻塞机制 | Java 层机制 | native 层阻塞机制 | native 层解除阻塞机制 )(二)
【Android 异步操作】Handler 机制 ( MessageQueue 消息队列的阻塞机制 | Java 层机制 | native 层阻塞机制 | native 层解除阻塞机制 )(二)
【Android 异步操作】Handler 机制 ( MessageQueue 消息队列的阻塞机制 | Java 层机制 | native 层阻塞机制 | native 层解除阻塞机制 )(一)
【Android 异步操作】Handler 机制 ( MessageQueue 消息队列的阻塞机制 | Java 层机制 | native 层阻塞机制 | native 层解除阻塞机制 )(一)
【Android 异步操作】Handler ( 主线程中的 Handler 与 Looper | Handler 原理简介 )
【Android 异步操作】Handler ( 主线程中的 Handler 与 Looper | Handler 原理简介 )
【Android 异步操作】线程池 ( 线程池使用示例 | 自定义线程池使用流程 | 自定义任务拒绝处理策略 | 完整代码示例 )
【Android 异步操作】线程池 ( 线程池使用示例 | 自定义线程池使用流程 | 自定义任务拒绝处理策略 | 完整代码示例 )
【Android 异步操作】线程池 ( 线程池作用 | 线程池种类 | 线程池工作机制 | 线程池任务调度源码解析 )
【Android 异步操作】线程池 ( 线程池作用 | 线程池种类 | 线程池工作机制 | 线程池任务调度源码解析 )
说到异步,脑海中立马浮现的就是多线程开发,Thread、Handler啥的一一涌上心头…
我们知道在Android开发中不能在非UI线程中更新UI,但是,有的时候我们需要在代码中执行一些诸如访问网络、查询数据库等耗时操作,为了不阻塞UI线程,我们时常会开启一个新的线程(工作线程)来执行这些耗时操作,然后我们可能需要将查询到的数据渲染到UI组件上,那么这个时候我们就需要考虑异步更新UI的问题了。
当我们的UI越来越复杂的时候,或者说某个业务需要大量的计算的时候,我们的主线程会消耗大量的资源去计算,这个时候,我们的Activity或者说fragmemt等UI页面就会出现卡顿,乃至ANR。