Android单元测试(八):怎样测试异步代码

简介: 异步无处不在,特别是网络请求,必须在子线程中执行。异步一般用来处理比较耗时的操作,除了网络请求外还有数据库操作、文件读写等等。一个典型的异步方法如下: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单元测试(八):怎样测试异步代码

目录
相关文章
|
测试技术 开发者 Python
Python单元测试入门:3个核心断言方法,帮你快速定位代码bug
本文介绍Python单元测试基础,详解`unittest`框架中的三大核心断言方法:`assertEqual`验证值相等,`assertTrue`和`assertFalse`判断条件真假。通过实例演示其用法,帮助开发者自动化检测代码逻辑,提升测试效率与可靠性。
746 1
|
算法 IDE Java
Java 项目实战之实际代码实现与测试调试全过程详解
本文详细讲解了Java项目的实战开发流程,涵盖项目创建、代码实现(如计算器与汉诺塔问题)、单元测试(使用JUnit)及调试技巧(如断点调试与异常排查),帮助开发者掌握从编码到测试调试的完整技能,提升Java开发实战能力。
1136 0
|
安全 Java 测试技术
《深入理解Spring》单元测试——高质量代码的守护神
Spring测试框架提供全面的单元与集成测试支持,通过`@SpringBootTest`、`@WebMvcTest`等注解实现分层测试,结合Mockito、Testcontainers和Jacoco,保障代码质量,提升开发效率与系统稳定性。
|
人工智能 边缘计算 搜索推荐
AI产品测试学习路径全解析:从业务场景到代码实践
本文深入解析AI测试的核心技能与学习路径,涵盖业务理解、模型指标计算与性能测试三大阶段,助力掌握分类、推荐系统、计算机视觉等多场景测试方法,提升AI产品质量保障能力。
|
测试技术 Go 开发者
如何为 gRPC Server 编写本地测试代码
本文介绍了如何使用 Go 语言中的 gRPC 测试工具 **bufconn**,通过内存连接实现 gRPC Server 的本地测试,避免端口冲突和外部依赖。结合示例代码,讲解了初始化内存监听、自定义拨号器及编写测试用例的完整流程,并借助断言库提升测试可读性与准确性。适用于单元及集成测试,助力高效开发。
366 1
|
人工智能 自然语言处理 测试技术
自然语言生成代码一键搞定!Codex CLI:OpenAI开源终端AI编程助手,代码重构+测试全自动
Codex CLI是OpenAI推出的轻量级AI编程智能体,基于自然语言指令帮助开发者高效生成代码、执行文件操作和进行版本控制,支持代码生成、重构、测试及数据库迁移等功能。
4166 1
自然语言生成代码一键搞定!Codex CLI:OpenAI开源终端AI编程助手,代码重构+测试全自动
|
安全 Java 测试技术
Java 项目实战中现代技术栈下代码实现与测试调试的完整流程
本文介绍基于Java 17和Spring技术栈的现代化项目开发实践。项目采用Gradle构建工具,实现模块化DDD分层架构,结合Spring WebFlux开发响应式API,并应用Record、Sealed Class等新特性。测试策略涵盖JUnit单元测试和Testcontainers集成测试,通过JFR和OpenTelemetry实现性能监控。部署阶段采用Docker容器化和Kubernetes编排,同时展示异步处理和反应式编程的性能优化。整套方案体现了现代Java开发的最佳实践,包括代码实现、测试调试
470 0
|
人工智能 自然语言处理 测试技术
Potpie.ai:比Copilot更狠!这个AI直接接管项目代码,自动Debug+测试+开发全搞定
Potpie.ai 是一个基于 AI 技术的开源平台,能够为代码库创建定制化的工程代理,自动化代码分析、测试和开发任务。
2047 19
Potpie.ai:比Copilot更狠!这个AI直接接管项目代码,自动Debug+测试+开发全搞定
|
存储 jenkins 测试技术
Apipost自动化测试:零代码!3步搞定!
传统手动测试耗时低效且易遗漏,全球Top 10科技公司中90%已转向自动化测试。Apipost无需代码,三步实现全流程自动化测试,支持小白快速上手。功能涵盖接口测试、性能压测与数据驱动,并提供动态数据提取、CICD集成等优势,助力高效测试全场景覆盖。通过拖拽编排、一键CLI生成,无缝对接Jenkins、GitHub Actions,提升测试效率与准确性。
1474 11

热门文章

最新文章