Contents

Awaitility 简介

1. 简介

异步系统的一个常见问题是很难为它们编写可读的测试,这些测试专注于业务逻辑并且不受同步、超时和并发控制的污染。

在本文中,我们将了解**Awaitility —— 一个为异步系统测试提供简单的领域特定语言 (DSL) 的库**。

使用 Awaitility,我们可以用易于阅读的 DSL 表达我们对系统的期望。

2. 依赖

我们需要将 Awaitility 依赖项添加到我们的pom.xml 中。

对于大多数用例来说,awaitility库就足够了。如果我们想使用基于代理的条件,我们还需要提供awaitility-proxy库:

<dependency>
    <groupId>org.awaitility</groupId>
    <artifactId>awaitility</artifactId>
    <version>3.0.0</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.awaitility</groupId>
    <artifactId>awaitility-proxy</artifactId>
    <version>3.0.0</version>
    <scope>test</scope>
</dependency>

您可以在 Maven Central 上找到最新版本的awaitilityawaitility-proxy 库。

3. 创建异步服务

让我们编写一个简单的异步服务并测试它:

public class AsyncService {
    private final int DELAY = 1000;
    private final int INIT_DELAY = 2000;
    private AtomicLong value = new AtomicLong(0);
    private Executor executor = Executors.newFixedThreadPool(4);
    private volatile boolean initialized = false;
    void initialize() {
        executor.execute(() -> {
            sleep(INIT_DELAY);
            initialized = true;
        });
    }
    boolean isInitialized() {
        return initialized;
    }
    void addValue(long val) {
        throwIfNotInitialized();
        executor.execute(() -> {
            sleep(DELAY);
            value.addAndGet(val);
        });
    }
    public long getValue() {
        throwIfNotInitialized();
        return value.longValue();
    }
    private void sleep(int delay) {
        try {
            Thread.sleep(delay);
        } catch (InterruptedException e) {
        }
    }
    private void throwIfNotInitialized() {
        if (!initialized) {
            throw new IllegalStateException("Service is not initialized");
        }
    }
}

4. 等待测试

现在,让我们创建测试类:

public class AsyncServiceLongRunningManualTest {
    private AsyncService asyncService;
    @Before
    public void setUp() {
        asyncService = new AsyncService();
    }
    
    //...
}

我们的测试检查我们的服务的初始化是否发生在调用初始化方法后指定的超时期限(默认 10 秒)内。

这个测试用例只是等待服务初始化状态改变,或者如果状态改变没有发生则抛出ConditionTimeoutException

状态由Callable获得,该 Callable在指定的初始延迟(默认 100 毫秒)后以定义的时间间隔(默认 100 毫秒)轮询我们的服务。在这里,我们使用超时、间隔和延迟的默认设置:

asyncService.initialize();
await()
  .until(asyncService::isInitialized);

在这里,我们使用await — Awaitility类的静态方法之一。它返回一个ConditionFactory类的实例。为了提高可读性,我们还可以使用其他方法,例如*given *。

可以使用Awaitility类中的静态方法更改默认时间参数:

Awaitility.setDefaultPollInterval(10, TimeUnit.MILLISECONDS);
Awaitility.setDefaultPollDelay(Duration.ZERO);
Awaitility.setDefaultTimeout(Duration.ONE_MINUTE);

在这里我们可以看到Duration类的使用,它为最常用的时间段提供了有用的常量。

我们还可以为每个await调用提供自定义计时值。在这里,我们期望初始化最多在 5 秒后发生,并且至少在 100 毫秒后发生,轮询间隔为 100 毫秒:

asyncService.initialize();
await()
    .atLeast(Duration.ONE_HUNDRED_MILLISECONDS)
    .atMost(Duration.FIVE_SECONDS)
  .with()
    .pollInterval(Duration.ONE_HUNDRED_MILLISECONDS)
    .until(asyncService::isInitialized);

值得一提的是,ConditionFactory包含额外的方法,例如with then和* given 。这些方法不做任何事情,只是返回this*,但它们可能有助于提高测试条件的可读性。

5. 使用匹配器

Awaitility 还允许使用hamcrest匹配器来检查表达式的结果。例如,我们可以在调用addValue方法后检查我们的long值是否按预期更改:

asyncService.initialize();
await()
  .until(asyncService::isInitialized);
long value = 5;
asyncService.addValue(value);
await()
  .until(asyncService::getValue, equalTo(value));

请注意,在此示例中,我们使用第一个await调用来等待服务初始化。否则,getValue方法将抛出IllegalStateException

6. 忽略异常

有时,我们会遇到一个方法在异步作业完成之前抛出异常的情况。在我们的服务中,可以在服务初始化之前调用getValue方法。

等待性提供了在不通过测试的情况下忽略此异常的可能性。

例如,让我们在初始化后检查getValue结果是否等于 0,忽略IllegalStateException

asyncService.initialize();
given().ignoreException(IllegalStateException.class)
  .await().atMost(Duration.FIVE_SECONDS)
  .atLeast(Duration.FIVE_HUNDRED_MILLISECONDS)
  .until(asyncService::getValue, equalTo(0L));

7. 使用代理

如第 2 节所述,我们需要包含awaitility-proxy以使用基于代理的条件。代理的想法是在不实现Callable或 lambda 表达式的情况下为条件提供真实的方法调用。

让我们使用AwaitilityClassProxy.to静态方法来检查AsyncService是否已初始化:

asyncService.initialize();
await()
  .untilCall(to(asyncService).isInitialized(), equalTo(true));

8. 访问字段

Awaitility 甚至可以访问私有字段以对其执行断言。在下面的示例中,我们可以看到另一种获取服务初始化状态的方法:

asyncService.initialize();
await()
  .until(fieldIn(asyncService)
  .ofType(boolean.class)
  .andWithName("initialized"), equalTo(true));