使用 Awaitility 测试异步代码
对于同步方法的测试很简单,调用完后可立马检查执行状态; 而异步方法,由于我们无法确切的知道何时结束,因此以往的办法是用
于是今天要介绍的 Awaitility 就应运而生了,专门针对异步方法的测试。它的官方文档在 https://github.com/awaitility/awaitility/wiki/Usage。本文主要关注在 Java 8 环境下用 Lambda 的代码书写方式。Awaitlity 实际运行是以某种轮询的方式来检查是否达到某个运行状态,可设定最多,最少等待时间,或永久等待,或自定义轮询策略,之后就开始进行需要的断言,所以它可以尽可能的节省测试异步方法所需的时长。而不像
通常我会在项目中给 JUnit 配上三个最佳伴侣,它们是(按
当然如果项目中没有异步调用自然是不需要 Awaitility, 在我的项目中是基本不可能的。以上三种都追求 DSL,以流畅的方式进行愉快的测试。
现在来尝试下 Awaitility 的几种基本的用法,先假定有下面的代码
我们把上面的
显然是不行的,当然是可以在
现在我们换成用 Awaitility 来进行上面的异步方法测试:
上面是最多等 600 毫秒,
Awaitility 并没有提供像
想让第一行代码像
使用 Awaitility 要做的事情说到底就是两件:

这里的 Matcher 可用标准的 Hamcrest matcher, 例如 org.hamcrest.CoreMatchers 下定义了许多的 matcher, is(T value), nullValue(), containsString(String substring) 等,代码
带 AtomicXxx 的方法大概也是方便是 Java 8 下使用,因为匿名类或 Lambda 只能访问外部的 final 变量,如果是 final 的 AtomicXxx 值就可以操作其中的值了
录制对 say(username) 方法的调用,并不是 Mock, 实际方法会被调用到
新的 UserService 类
第一种方式,试图获取捕获的参数,直到不抛出 AssertionError 异常为止
如果 userDao.add(username) 有返回值的话,可以用 untilCall() 方法来捕获,相关代码
第二种方式,使用 AtomicBoolean 来记录方法是否被调用
从控制台的输出就能了解它与 Thread.sleep() 是要更为高效的, 而不是痴痴的等
2019-07-17 补充:
Mockito 自 1.8.5 开始在 verify 时就可以用 timeout 方法,见 Verification with timeout (Since 1.8.5)。示例代码如下:
Awaitility 3.x 开始加入了 untilAsserted() 方法可与 AssertJ 或 Fest Assert 进行交互,官方代码
Awaitility 3.x 或以后
Awaitility 2.x 及以下
其实就是一直尝试去断言
参考
永久链接 https://yanbin.blog/test-asyn-call-with-awaitility/, 来自 隔叶黄莺 Yanbin's Blog
[版权声明]
本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。
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 中的显示方式):- org.awaitility:awaitility:2.0.0:test
- org.assertj:assertj-core: version: 3.8.0:test
- org.mockito:mockito-core:2.7.22:test
当然如果项目中没有异步调用自然是不需要 Awaitility, 在我的项目中是基本不可能的。以上三种都追求 DSL,以流畅的方式进行愉快的测试。
现在来尝试下 Awaitility 的几种基本的用法,先假定有下面的代码
UserService 1package cc.unmi;
2
3import java.util.ArrayList;
4import java.util.List;
5import java.util.concurrent.ThreadLocalRandom;
6import java.util.concurrent.TimeUnit;
7
8public class UserService {
9 public final List<String> users = new ArrayList<>();
10
11 public void addUser(String username) {
12 new Thread(() -> {
13 try {
14 TimeUnit.MILLISECONDS.sleep(ThreadLocalRandom.current().nextLong(100, 500));
15 } catch (InterruptedException e) {
16 }
17 users.add(username);
18
19 }).start();
20 }
21}我们把上面的
addUser() 方法做成了一个异步的,而且执行时间是不定的,此处设定在 100 至 500 毫秒之间,如果用常规的测试方式1@Test
2public void couldAddUserAsync() throws Exception {
3 UserService userService = new UserService();
4 userService.addUser("Yanbin");
5
6 //Thread.sleep(600);
7 assertThat(userService.users).contains("Yanbin");
8}显然是不行的,当然是可以在
assertThat() 前面加上 Thread.sleep(600), 但每次都会浪费平均大概 300 毫秒的时间,好像也不怎么多,但大量采用这种方式就可观了。用 Thread.sleep(600) 的方式该测试用例在 IntelliJ IDEA 中大概需要 780 毫秒, 每次至少都要 600 毫秒。现在我们换成用 Awaitility 来进行上面的异步方法测试:
Lambda 与 AssertJ 的方式
这是我比较喜欢的断定方式1@Test
2public void couldAddUserAsync() {
3 UserService userService = new UserService();
4 userService.addUser("Yanbin");
5
6 Awaitility.await().atMost(600, MILLISECONDS).until(() ->
7 assertThat(userService.users).contains("Yanbin"));
8}上面是最多等 600 毫秒,
until() 方法的原型是 until(Runnable supplier), 也就是直到不抛出 AssertionError 异常为止,否则测试会出现异常 ConditionTimeoutException, 并且告知条件不满足的详情。这种方式平均时间在 500 毫秒,最快时 100 多毫秒。Awaitility 并没有提供像
Thread.sleep(600) 那样傻傻的等上 600 毫秒的机制,它总是需要一个测试条件。看到上面的代码好像是在等 600 毫秒,其实内部实现是作了一个默认每隔 100 毫秒的一个轮询,并且在指定的时间内测定条件通过后才往下走,否则是 ConditionTimeoutException 异常。所以试图写成1await().atLeast(600, MILLISECONDS); //并不会等待,而是立即返回
2
3assertThat(userService.users).contains("Yanbin");想让第一行代码像
Thread.sleep(600) 一样工作是徒劳的,用 Awaitility 必须给它提供一个测试条件。使用 Awaitility 要做的事情说到底就是两件:
- 如何设置轮询策略, atLeast, atMost 或默认超时为 10 秒,默认 100 毫秒的轮询间隔,或 斐波那契数列 间隔,或完全自定义; 还能永久等待(如果你愿意的话)
- 测定条件,直到某个方法被调用,直到数据库表中出现某行记录等等。这就是
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) 等,代码
1await().until(userDao.findByName("Yanbin"), notNullValue());
2
3await().until(userDao::findAll, hasItm("Yanbin")); //Java 8 就是方便带 AtomicXxx 的方法大概也是方便是 Java 8 下使用,因为匿名类或 Lambda 只能访问外部的 final 变量,如果是 final 的 AtomicXxx 值就可以操作其中的值了
1AtomicInterger atomic = new AtomicInteger(0);
2//做些操作来改变 atomic 中的值,比如 Mockito 的桩方法
3
4await().untilAtomic(atomic, equalTo(1));untilCall 代理方法调用
演示实例,需要对前面 UserService 稍加改变,在添加完用户到 users 之后,调用一下 say(username) 方法 1public class UserService {
2 public final List<String> users = new ArrayList<>();
3
4 public void addUser(String username) {
5 new Thread(() -> {
6 try {
7 TimeUnit.MILLISECONDS.sleep(ThreadLocalRandom.current().nextLong(100, 500));
8 } catch (InterruptedException e) {
9 }
10
11 users.add(username);
12 say(username);
13 }).start();
14 }
15
16 public String say(String username) {
17 return "Hello " + username;
18 }
19}录制对 say(username) 方法的调用,并不是 Mock, 实际方法会被调用到
1@Test
2public void couldAddUserAsync() {
3 UserService userService = new UserService();
4 userService.addUser("Yanbin");
5
6 //直到 say("Yanbin") 被调用,并且返回值为 "Hello Yanbin", say("Yanbin") 需要精确的参数值
7 await().untilCall(to(userService).say("Yanbin"), equalTo("Hello Yanbin"));
8 assertThat(userService.users).contains("Yanbin");
9}Awaitility 与 Mock(以 Mockito 为例)
我们需要对待测试代码再做改变,引入一个UserDao 接口1package cc.unmi;
2
3interface UserDao {
4 void add(String username);
5}新的 UserService 类
1package cc.unmi;
2
3import java.util.concurrent.ThreadLocalRandom;
4import java.util.concurrent.TimeUnit;
5
6public class UserService {
7 private UserDao userDao;
8
9 public UserService(UserDao userDao) {
10 this.userDao = userDao;
11 }
12
13 public void addUser(String username) {
14 new Thread(() -> {
15 try {
16 TimeUnit.MILLISECONDS.sleep(ThreadLocalRandom.current().nextLong(100, 500));
17 } catch (InterruptedException e) {
18 }
19
20 userDao.add(username);
21 }).start();
22 }
23}第一种方式,试图获取捕获的参数,直到不抛出 AssertionError 异常为止
1@RunWith(MockitoJUnitRunner.class)
2public class UserServiceTest {
3 @Test
4 public void couldAddUserAsync() {
5 UserDao userDao = Mockito.mock(UserDao.class);
6 UserService userService = new UserService(userDao);
7
8 ArgumentCaptor<String> usernameCaptor = ArgumentCaptor.forClass(String.class);
9 doNothing().when(userDao).add(usernameCaptor.capture()); //对 userDao.add(username) 方法打桩,由于无返回值,所以可用 doNothing()
10
11 userService.addUser("Yanbin");
12
13 await().until(() -> { //until(Runnable), 直到不抛出 AssertionError 异常为止,所以需要把 MockitoException 转换为 AssertionError
14 try {
15 usernameCaptor.getValue();
16 } catch (Exception ex) {
17 throw new AssertionError(ex.getMessage());
18 }
19 });
20
21 assertThat(usernameCaptor.getValue()).isEqualTo("Yanbin");
22 }
23} 如果 userDao.add(username) 有返回值的话,可以用 untilCall() 方法来捕获,相关代码
1when(userDao.add(usernameCaptor.capture).thenReturn("Added Yanbin");
2await().untilCall(to(userDao).add("Yanbin"), equalTo("Added Yanbin"));
3assertThat(usernameCaptor.getValue()).isEqualTo("Yanbin");第二种方式,使用 AtomicBoolean 来记录方法是否被调用
1@RunWith(MockitoJUnitRunner.class)
2public class UserServiceTest {
3
4 @Test
5 public void couldAddUserAsync() {
6 UserDao userDao = Mockito.mock(UserDao.class);
7 UserService userService = new UserService(userDao);
8
9 ArgumentCaptor<String> usernameCaptor = ArgumentCaptor.forClass(String.class);
10
11 AtomicBoolean addMethodCalled = new AtomicBoolean(false);
12 doAnswer(invocationOnMock -> addMethodCalled.getAndSet(true)) //其实可以不用 usernameCaptor, 通过 invocationOnMock 也能获得参数
13 .when(userDao).add(usernameCaptor.capture());
14
15 userService.addUser("Yanbin");
16
17 await().untilTrue(addMethodCalled);
18 assertThat(usernameCaptor.getValue()).isEqualTo("Yanbin");
19 }
20}对属性值的测定
我们自己能够以反射的方式来测定直到预期的对象内部状态,为方便起见,Awaitility 还为我们提供了几个方法来窥视属性值,下面的例子代码直接从官方 Wiki 拷过来的1await().until(fieldIn(object).ofType(int.class). equalTo(2));
2
3await().until(fieldIn(object).ofType(int.class).andWithName("fieldName"), equalTo(2));
4
5await().until(fieldIn(object).ofType(int.class).andAnnotatedWith(MyAnnotation.class), equalTo(2));感受默认轮询行为
如果不清楚 atMost(...).until(...) 内部做了什么,可以用ConditionEvalutionListener 来查看,像下面的代码1await().atMost(600, MILLISECONDS)
2 .conditionEvaluationListener(new ConditionEvaluationLogger())
3 .until(() ->
4 assertThat(userService.users).contains("Yanbin"));从控制台的输出就能了解它与 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))内置的轮询策略基本能满足我们的需求,所以对于如何自定义轮询不进行深入,详情请见官方 Wiki.
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)
2019-07-17 补充:
Mockito 自 1.8.5 开始在 verify 时就可以用 timeout 方法,见 Verification with timeout (Since 1.8.5)。示例代码如下:
1//在 500 毫秒之内有两次对 mock.someMethod() 方法的调用
2//此处最多等待 500 毫秒
3verify(mock, timeout(500).times(2)).someMethod();Awaitility 3.x 开始加入了 untilAsserted() 方法可与 AssertJ 或 Fest Assert 进行交互,官方代码
Awaitility 3.x 或以后
1await().atMost(5, SECONDS).untilAsserted(() -> assertThat(fakeRepository.getValue()).isEqualTo(1));Awaitility 2.x 及以下
1await().atMost(5, SECONDS).until(() -> assertThat(fakeRepository.getValue()).isEqualTo(1));其实就是一直尝试去断言
assertThat(fakeRepository.getValue()).isEqualTo(1), 5 秒钟之内不会有一次成功就算通过测试(直到不抛出 java.lang.AssertionError 错误为止)。参考
永久链接 https://yanbin.blog/test-asyn-call-with-awaitility/, 来自 隔叶黄莺 Yanbin's Blog
[版权声明]
本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。