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则不能实现这种异步方法的测试了。

实际上,可以使用synchronizedwait/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单元测试(八):怎样测试异步代码

目录
相关文章
|
3月前
|
数据采集 机器学习/深度学习 大数据
行为检测代码(一):超详细介绍C3D架构训练+测试步骤
这篇文章详细介绍了C3D架构在行为检测领域的应用,包括训练和测试步骤,使用UCF101数据集进行演示。
117 1
行为检测代码(一):超详细介绍C3D架构训练+测试步骤
|
3月前
|
机器学习/深度学习 人工智能 监控
提升软件质量的关键路径:高效测试策略与实践在软件开发的宇宙中,每一行代码都如同星辰般璀璨,而将这些星辰编织成星系的过程,则依赖于严谨而高效的测试策略。本文将引领读者探索软件测试的奥秘,揭示如何通过精心设计的测试方案,不仅提升软件的性能与稳定性,还能加速产品上市的步伐,最终实现质量与效率的双重飞跃。
在软件工程的浩瀚星海中,测试不仅是发现缺陷的放大镜,更是保障软件质量的坚固防线。本文旨在探讨一种高效且创新的软件测试策略框架,它融合了传统方法的精髓与现代技术的突破,旨在为软件开发团队提供一套系统化、可执行性强的测试指引。我们将从测试规划的起点出发,沿着测试设计、执行、反馈再到持续优化的轨迹,逐步展开论述。每一步都强调实用性与前瞻性相结合,确保测试活动能够紧跟软件开发的步伐,及时适应变化,有效应对各种挑战。
|
1天前
|
前端开发 JavaScript 测试技术
使用ChatGPT生成登录产品代码的测试用例和测试脚本
使用ChatGPT生成登录产品代码的测试用例和测试脚本
53 35
|
1天前
|
JavaScript 前端开发 Java
使用ChatGPT生成关于登录产品代码的单元测试代码
使用ChatGPT生成关于登录产品代码的单元测试代码
32 16
|
28天前
|
算法 Java 测试技术
使用 BenchmarkDotNet 对 .NET 代码进行性能基准测试
使用 BenchmarkDotNet 对 .NET 代码进行性能基准测试
57 13
|
2月前
|
并行计算 算法 测试技术
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面,旨在通过综合策略提升程序性能,满足实际需求。
84 1
|
2月前
|
JavaScript 安全 编译器
TypeScript 与 Jest 测试框架的结合使用,从 TypeScript 的测试需求出发,介绍了 Jest 的特点及其与 TypeScript 结合的优势,详细讲解了基本测试步骤、常见测试场景及异步操作测试方法
本文深入探讨了 TypeScript 与 Jest 测试框架的结合使用,从 TypeScript 的测试需求出发,介绍了 Jest 的特点及其与 TypeScript 结合的优势,详细讲解了基本测试步骤、常见测试场景及异步操作测试方法,并通过实际案例展示了其在项目中的应用效果,旨在提升代码质量和开发效率。
64 6
|
2月前
|
测试技术 开发者 UED
探索软件测试的深度:从单元测试到自动化测试
【10月更文挑战第30天】在软件开发的世界中,测试是确保产品质量和用户满意度的关键步骤。本文将深入探讨软件测试的不同层次,从基本的单元测试到复杂的自动化测试,揭示它们如何共同构建一个坚实的质量保证体系。我们将通过实际代码示例,展示如何在开发过程中实施有效的测试策略,以确保软件的稳定性和可靠性。无论你是新手还是经验丰富的开发者,这篇文章都将为你提供宝贵的见解和实用技巧。
|
3月前
|
安全 Java 网络安全
Android远程连接和登录FTPS服务代码(commons.net库)
Android远程连接和登录FTPS服务代码(commons.net库)
49 1
|
3月前
|
Android开发 Swift iOS开发
探索安卓与iOS开发的差异:从代码到用户体验
【10月更文挑战第5天】在移动应用开发的广阔天地中,安卓和iOS两大平台各占半壁江山。它们在技术架构、开发环境及用户体验上有着根本的不同。本文通过比较这两种平台的开发过程,揭示背后的设计理念和技术选择如何影响最终产品。我们将深入探讨各自平台的代码示例,理解开发者面临的挑战,以及这些差异如何塑造用户的日常体验。

热门文章

最新文章