对于同步方法的测试很简单,调用完后可立马检查执行状态; 而异步方法,由于我们无法确切的知道何时结束,因此以往的办法是用 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
package cc.unmi; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; public class UserService { public final List<String> users = new ArrayList<>(); public void addUser(String username) { new Thread(() -> { try { TimeUnit.MILLISECONDS.sleep(ThreadLocalRandom.current().nextLong(100, 500)); } catch (InterruptedException e) { } users.add(username); }).start(); } } |
我们把上面的 addUser()
方法做成了一个异步的,而且执行时间是不定的,此处设定在 100 至 500 毫秒之间,如果用常规的测试方式
1 2 3 4 5 6 7 8 |
@Test public void couldAddUserAsync() throws Exception { UserService userService = new UserService(); userService.addUser("Yanbin"); //Thread.sleep(600); assertThat(userService.users).contains("Yanbin"); } |
显然是不行的,当然是可以在 assertThat()
前面加上 Thread.sleep(600)
, 但每次都会浪费平均大概 300 毫秒的时间,好像也不怎么多,但大量采用这种方式就可观了。用 Thread.sleep(600)
的方式该测试用例在 IntelliJ IDEA 中大概需要 780 毫秒, 每次至少都要 600 毫秒。
现在我们换成用 Awaitility 来进行上面的异步方法测试:
Lambda 与 AssertJ 的方式
这是我比较喜欢的断定方式
1 2 3 4 5 6 7 8 |
@Test public void couldAddUserAsync() { UserService userService = new UserService(); userService.addUser("Yanbin"); Awaitility.await().atMost(600, MILLISECONDS).until(() -> assertThat(userService.users).contains("Yanbin")); } |
上面是最多等 600 毫秒,until()
方法的原型是 until(Runnable supplier)
, 也就是直到不抛出 AssertionError
异常为止,否则测试会出现异常 ConditionTimeoutException
, 并且告知条件不满足的详情。这种方式平均时间在 500 毫秒,最快时 100 多毫秒。
Awaitility 并没有提供像 Thread.sleep(600)
那样傻傻的等上 600 毫秒的机制,它总是需要一个测试条件。看到上面的代码好像是在等 600 毫秒,其实内部实现是作了一个默认每隔 100 毫秒的一个轮询,并且在指定的时间内测定条件通过后才往下走,否则是 ConditionTimeoutException
异常。所以试图写成
1 2 3 |
await().atLeast(600, MILLISECONDS); //并不会等待,而是立即返回 assertThat(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) 等,代码
1 2 3 |
await().until(userDao.findByName("Yanbin"), notNullValue()); await().until(userDao::findAll, hasItm("Yanbin")); //Java 8 就是方便 |
带 AtomicXxx 的方法大概也是方便是 Java 8 下使用,因为匿名类或 Lambda 只能访问外部的 final 变量,如果是 final 的 AtomicXxx 值就可以操作其中的值了
1 2 3 4 |
AtomicInterger atomic = new AtomicInteger(0); //做些操作来改变 atomic 中的值,比如 Mockito 的桩方法 await().untilAtomic(atomic, equalTo(1)); |
untilCall 代理方法调用
演示实例,需要对前面 UserService 稍加改变,在添加完用户到 users 之后,调用一下 say(username) 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public class UserService { public final List<String> users = new ArrayList<>(); public void addUser(String username) { new Thread(() -> { try { TimeUnit.MILLISECONDS.sleep(ThreadLocalRandom.current().nextLong(100, 500)); } catch (InterruptedException e) { } users.add(username); say(username); }).start(); } public String say(String username) { return "Hello " + username; } } |
录制对 say(username) 方法的调用,并不是 Mock, 实际方法会被调用到
1 2 3 4 5 6 7 8 9 |
@Test public void couldAddUserAsync() { UserService userService = new UserService(); userService.addUser("Yanbin"); //直到 say("Yanbin") 被调用,并且返回值为 "Hello Yanbin", say("Yanbin") 需要精确的参数值 await().untilCall(to(userService).say("Yanbin"), equalTo("Hello Yanbin")); assertThat(userService.users).contains("Yanbin"); } |
Awaitility 与 Mock(以 Mockito 为例)
我们需要对待测试代码再做改变,引入一个 UserDao
接口
1 2 3 4 5 |
package cc.unmi; interface UserDao { void add(String username); } |
新的 UserService 类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
package cc.unmi; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; public class UserService { private UserDao userDao; public UserService(UserDao userDao) { this.userDao = userDao; } public void addUser(String username) { new Thread(() -> { try { TimeUnit.MILLISECONDS.sleep(ThreadLocalRandom.current().nextLong(100, 500)); } catch (InterruptedException e) { } userDao.add(username); }).start(); } } |
第一种方式,试图获取捕获的参数,直到不抛出 AssertionError 异常为止
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
@RunWith(MockitoJUnitRunner.class) public class UserServiceTest { @Test public void couldAddUserAsync() { UserDao userDao = Mockito.mock(UserDao.class); UserService userService = new UserService(userDao); ArgumentCaptor<String> usernameCaptor = ArgumentCaptor.forClass(String.class); doNothing().when(userDao).add(usernameCaptor.capture()); //对 userDao.add(username) 方法打桩,由于无返回值,所以可用 doNothing() userService.addUser("Yanbin"); await().until(() -> { //until(Runnable), 直到不抛出 AssertionError 异常为止,所以需要把 MockitoException 转换为 AssertionError try { usernameCaptor.getValue(); } catch (Exception ex) { throw new AssertionError(ex.getMessage()); } }); assertThat(usernameCaptor.getValue()).isEqualTo("Yanbin"); } } |
如果 userDao.add(username) 有返回值的话,可以用 untilCall() 方法来捕获,相关代码
1 2 3 |
when(userDao.add(usernameCaptor.capture).thenReturn("Added Yanbin"); await().untilCall(to(userDao).add("Yanbin"), equalTo("Added Yanbin")); assertThat(usernameCaptor.getValue()).isEqualTo("Yanbin"); |
第二种方式,使用 AtomicBoolean 来记录方法是否被调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
@RunWith(MockitoJUnitRunner.class) public class UserServiceTest { @Test public void couldAddUserAsync() { UserDao userDao = Mockito.mock(UserDao.class); UserService userService = new UserService(userDao); ArgumentCaptor<String> usernameCaptor = ArgumentCaptor.forClass(String.class); AtomicBoolean addMethodCalled = new AtomicBoolean(false); doAnswer(invocationOnMock -> addMethodCalled.getAndSet(true)) //其实可以不用 usernameCaptor, 通过 invocationOnMock 也能获得参数 .when(userDao).add(usernameCaptor.capture()); userService.addUser("Yanbin"); await().untilTrue(addMethodCalled); assertThat(usernameCaptor.getValue()).isEqualTo("Yanbin"); } } |
对属性值的测定
我们自己能够以反射的方式来测定直到预期的对象内部状态,为方便起见,Awaitility 还为我们提供了几个方法来窥视属性值,下面的例子代码直接从官方 Wiki 拷过来的
1 2 3 4 5 |
await().until(fieldIn(object).ofType(int.class). equalTo(2)); await().until(fieldIn(object).ofType(int.class).andWithName("fieldName"), equalTo(2)); await().until(fieldIn(object).ofType(int.class).andAnnotatedWith(MyAnnotation.class), equalTo(2)); |
感受默认轮询行为
如果不清楚 atMost(...).until(...) 内部做了什么,可以用 ConditionEvalutionListener
来查看,像下面的代码
1 2 3 4 |
await().atMost(600, MILLISECONDS) .conditionEvaluationListener(new ConditionEvaluationLogger()) .until(() -> 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))
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.
2019-07-17 补充:
Mockito 自 1.8.5 开始在 verify 时就可以用 timeout 方法,见 Verification with timeout (Since 1.8.5)。示例代码如下:
1 2 3 |
//在 500 毫秒之内有两次对 mock.someMethod() 方法的调用 //此处最多等待 500 毫秒 verify(mock, timeout(500).times(2)).someMethod(); |
Awaitility 3.x 开始加入了 untilAsserted() 方法可与 AssertJ 或 Fest Assert 进行交互,官方代码
Awaitility 3.x 或以后
1 |
await().atMost(5, SECONDS).untilAsserted(() -> assertThat(fakeRepository.getValue()).isEqualTo(1)); |
Awaitility 2.x 及以下
1 |
await().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 Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。
[…] 现在来尝试下 Awaitility 的几种基本的用法,先假定有下面的代码 UserService 阅读全文 >> […]
希望去掉>和<
语法着色的插件升级了,很多代码都乱了,谢谢!