异步编程测试Awaitlity简介| Baeldung


异步系统的一个常见问题是,很难为那些专注于业务逻辑并且不会受到同步,超时和并发控制污染的可编写测试。
在本文中,我们将介绍Awaitility - 一个为异步系统测试提供简单的特定于域的语言(DSL)的库。
通过Awaitility,我们可以通过易于阅读的DSL表达我们对系统的期望。
我们需要将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库。

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

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");
        }
    }
}

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

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

我们的测试在调用initialize方法后检查我们的服务初始化是否在指定的超时时间(默认为10秒)内发生。
此测试用例仅等待服务初始化状态更改或在未发生状态更改时抛出ConditionTimeoutException。
状态由Callable获得,Callable在指定的初始延迟(默认为100毫秒)后以定义的时间间隔(默认为100毫秒)轮询我们的服务。这里我们使用超时,间隔和延迟的默认设置:

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

在这里,我们使用await - Awaitility类的静态方法之一。它返回ConditionFactory类的实例。我们也可以用其他的方法,如给予增加可读性的原因。
可以使用Awaitility类中的静态方法更改默认计时参数:

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

在这里我们可以看到Duration类的使用,它为最常用的时间段提供了有用的常量。
我们还可以为每个等待呼叫提供自定义时间值。在这里,我们期望初始化将在五秒后发生,并且至少在100ms之后,轮询间隔为100ms:

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

值得一提的是,ConditionFactory包含像其他方法with, then, and, given。这些方法没有做任何事情,只是返回它,但它们可能有助于增强测试条件的可读性。

使用匹配器:
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。

忽略例外:
有时,我们遇到一种情况,即方法在异步作业完成之前抛出异常。在我们的服务中,它可以是在初始化服务之前调用getValue方法。
Awaitility提供了在不失败测试的情况下忽略此异常的可能性。
例如,让我们在初始化之后检查getValue结果是否等于零,忽略IllegalStateException:

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

使用代理:
我们使用awaitility-proxy以实现基于代理的条件判断。代理的想法是为条件提供真正的方法调用,而不实现Callable或lambda表达式。
让我们使用AwaitilityClassProxy.to静态方法来检查AsyncService是否已初始化:

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

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

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

Github上提供所有代码示例

如何测试@Scheduled
Spring Framework中的一个可用注释是@Scheduled。我们可以使用此批注以预定的方式执行任务
让我们开始创建一个简单的Counter类:

@Component
public class Counter {
    private AtomicInteger count = new AtomicInteger(0);
 
    @Scheduled(fixedDelay = 5)
    public void scheduled() {
        this.count.incrementAndGet();
    }
 
    public int getInvocationCount() {
        return this.count.get();
    }
}

我们将使用预定的方法来增加计数。请注意,我们还添加了@Scheduled注释,以在5毫秒的固定时间内执行它。
另外,让我们创建一个ScheduledConfig类,使用@EnableScheduling注释启用计划任务:

@Configuration
@EnableScheduling
@ComponentScan("com.baeldung.scheduled")
public class ScheduledConfig {
}

我们可以使用Awaitility DSL使我们的测试更具说明性:

@SpringJUnitConfig(ScheduledConfig.class)
public class ScheduledAwaitilityIntegrationTest {
 
    @SpyBean
    private Counter counter;
 
    @Test
    public void whenWaitOneSecond_thenScheduledIsCalledAtLeastTenTimes() {
        await()
          .atMost(Duration.ONE_SECOND)
          .untilAsserted(() -> verify(counter, atLeast(10)).scheduled());
    }
}

在这种情况下,我们使用@SpyBean注释注入bean,以检查在  一秒的时间内调用调度方法的次数。
完整源码GitHub上获得