使用 Awaitility 测试异步代码

对于同步方法的测试很简单,调用完后可立马检查执行状态; 而异步方法,由于我们无法确切的知道何时结束,因此以往的办法是用 Thread.sleep(500) 来预估一个执行时间。然后通常我们估计的要长于实际的时间,这就很浪费,况且偶然的超过预估的等待时间也并不意味着代码有问题。还有 sleep 方法还抛出一个检测异常 InterruptedException, 一般会要对 Thread.sleep(500) 作下简单包装。

于是今天要介绍的 Awaitility 就应运而生了,专门针对异步方法的测试。它的官方文档在 https://github.com/awaitility/awaitility/wiki/Usage。本文主要关注在 Java 8 环境下用 Lambda 的代码书写方式。Awaitlity 实际运行是以某种轮询的方式来检查是否达到某个运行状态,可设定最多,最少等待时间,或永久等待,或自定义轮询策略,之后就开始进行需要的断言,所以它可以尽可能的节省测试异步方法所需的时长。而不像 Thread.sleep(500) 一路等到黑,并且没有回头路。

通常我会在项目中给 JUnit 配上三个最佳伴侣,它们是(按 mvn dependency:tree 中的显示方式):

  1. org.awaitility:awaitility:2.0.0:test
  2. org.assertj:assertj-core: version: 3.8.0:test
  3. org.mockito:mockito-core:2.7.22:test

当然如果项目中没有异步调用自然是不需要 Awaitility, 在我的项目中是基本不可能的。以上三种都追求 DSL,以流畅的方式进行愉快的测试。

现在来尝试下 Awaitility 的几种基本的用法,先假定有下面的代码 UserService

我们把上面的 addUser() 方法做成了一个异步的,而且执行时间是不定的,此处设定在 100 至 500 毫秒之间,如果用常规的测试方式

显然是不行的,当然是可以在 assertThat() 前面加上 Thread.sleep(600), 但每次都会浪费平均大概 300 毫秒的时间,好像也不怎么多,但大量采用这种方式就可观了。用 Thread.sleep(600) 的方式该测试用例在 IntelliJ IDEA 中大概需要 780 毫秒, 每次至少都要 600 毫秒。

现在我们换成用 Awaitility 来进行上面的异步方法测试:

Lambda 与 AssertJ 的方式

这是我比较喜欢的断定方式

上面是最多等 600 毫秒,until() 方法的原型是 until(Runnable supplier), 也就是直到不抛出 AssertionError 异常为止,否则测试会出现异常 ConditionTimeoutException, 并且告知条件不满足的详情。这种方式平均时间在 500 毫秒,最快时 100 多毫秒。

Awaitility 并没有提供像 Thread.sleep(600) 那样傻傻的等上 600 毫秒的机制,它总是需要一个测试条件。看到上面的代码好像是在等 600 毫秒,其实内部实现是作了一个默认每隔 100 毫秒的一个轮询,并且在指定的时间内测定条件通过后才往下走,否则是 ConditionTimeoutException 异常。所以试图写成

想让第一行代码像 Thread.sleep(600) 一样工作是徒劳的,用 Awaitility 必须给它提供一个测试条件。

使用 Awaitility 要做的事情说到底就是两件:

  1. 如何设置轮询策略, atLeast, atMost 或默认超时为 10 秒,默认 100 毫秒的轮询间隔,或 斐波那契数列 间隔,或完全自定义; 还能永久等待(如果你愿意的话)
  2. 测定条件,直到某个方法被调用,直到数据库表中出现某行记录等等。这就是 ConditionFactory 的所有 until 方法要做的事情,见下图

了解 untilXxx(...) 方法

  • until(Callable<Boolean> conditionEvaluator):  直到 Callable 返回值为 true
  • until(Runnable supplier): 上面提到过,直到没有 AssertionError 异常为止,所以可于 Fest Assert 很好的工作
  • untilFalse(AtomicBoolean atomic), untilTrue(AtomicBoolean atomic) 用于测试 AtomicBoolean 变量是否为假,或真
  • untilCall(T ignore, Matcher<? super T> matcher) 会让代理记录下对有返回值实际的调用

这里的 Matcher 可用标准的 Hamcrest matcher, 例如 org.hamcrest.CoreMatchers 下定义了许多的 matcher, is(T value), nullValue(), containsString(String substring) 等,代码

带 AtomicXxx 的方法大概也是方便是 Java 8 下使用,因为匿名类或 Lambda 只能访问外部的 final 变量,如果是 final 的 AtomicXxx 值就可以操作其中的值了

untilCall 代理方法调用

演示实例,需要对前面 UserService 稍加改变,在添加完用户到 users 之后,调用一下 say(username) 方法

录制对 say(username) 方法的调用,并不是 Mock, 实际方法会被调用到

Awaitility 与 Mock(以 Mockito 为例)

我们需要对待测试代码再做改变,引入一个 UserDao 接口

新的 UserService 类

第一种方式,试图获取捕获的参数,直到不抛出 AssertionError 异常为止

如果 userDao.add(username) 有返回值的话,可以用 untilCall() 方法来捕获,相关代码

第二种方式,使用 AtomicBoolean 来记录方法是否被调用

对属性值的测定

我们自己能够以反射的方式来测定直到预期的对象内部状态,为方便起见,Awaitility 还为我们提供了几个方法来窥视属性值,下面的例子代码直接从官方 Wiki 拷过来的

感受默认轮询行为

如果不清楚 atMost(...).until(...) 内部做了什么,可以用 ConditionEvalutionListener 来查看,像下面的代码

从控制台的输出就能了解它与 Thread.sleep() 是要更为高效的, 而不是痴痴的等

Condition defined as a lambda expression in cc.unmi.UserServiceTest that uses cc.unmi.UserService <[]> does not contain element(s):<['Yanbin']> (elapsed time 125 milliseconds, remaining time 475 milliseconds (last poll interval was 100 milliseconds))
Condition defined as a lambda expression in cc.unmi.UserServiceTest that uses cc.unmi.UserService <[]> does not contain element(s):<['Yanbin']> (elapsed time 232 milliseconds, remaining time 368 milliseconds (last poll interval was 100 milliseconds))
Condition defined as a lambda expression in cc.unmi.UserServiceTest that uses cc.unmi.UserService <[]> does not contain element(s):<['Yanbin']> (elapsed time 337 milliseconds, remaining time 263 milliseconds (last poll interval was 100 milliseconds))
Condition defined as a lambda expression in cc.unmi.UserServiceTest that uses cc.unmi.UserService reached its end value after 440 milliseconds (remaining time 160 milliseconds, last poll interval was 100 milliseconds)

内置的轮询策略基本能满足我们的需求,所以对于如何自定义轮询不进行深入,详情请见官方 Wiki.

参考

  1. Awaitility 官方文档

类别: Java/JEE. 标签: . 阅读(214). 订阅评论. TrackBack.

Leave a Reply

Be the First to Comment!

avatar