1. 概述

异步系统测试中最大的痛点是什么?测试代码被大量同步逻辑、超时控制和并发管理污染,导致业务逻辑被淹没。

Awaitility 就是专门解决这个问题的利器——它提供了一套简洁的领域特定语言(DSL),专门用于异步系统测试。

✅ 核心优势:用接近自然语言的DSL表达测试预期,让测试代码像业务逻辑一样清晰。


2. 依赖配置

pom.xml 中添加两个依赖:

<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>

⚠️ 注意:

  • awaitility 核心库满足大部分场景
  • awaitility-proxy 是可选的,用于代理模式的高级用法
  • 最新版本可在 Maven Central 获取

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

🔍 核心逻辑:

  • 初始化延迟2秒 (INIT_DELAY)
  • 值操作延迟1秒 (DELAY)
  • 未初始化时调用 getValue() 直接抛异常

4. Awaitility 测试实战

创建测试类:

public class AsyncServiceLongRunningManualTest {
    private AsyncService asyncService;

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

4.1 基础用法

测试服务初始化(默认超时10秒):

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

💡 拆解:

  • await() 返回 ConditionFactory 实例
  • 默认轮询间隔100ms,初始延迟100ms
  • 超时抛出 ConditionTimeoutException

4.2 自定义时间参数

通过静态方法修改全局默认值:

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

或为单个测试定制参数:

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

📌 参数说明:
| 方法 | 含义 | |---------------------|--------------------------| | atLeast() | 最小等待时间 | | atMost() | 最大超时时间 | | pollInterval() | 轮询间隔 | | with() | 增强可读性的语法糖 |


5. 使用 Hamcrest 匹配器

结合 Hamcrest 进行复杂断言:

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

⚠️ 踩坑提醒:务必先等待初始化完成,否则 getValue() 会直接抛异常!


6. 异常忽略策略

某些场景下方法可能暂时抛异常(如未初始化时调用 getValue()),可用 ignoreException() 忽略:

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

🔍 适用场景:

  • 方法在异步操作完成前临时抛异常
  • 需要验证最终状态而非中间状态

7. 代理模式进阶

引入 awaitility-proxy 后,可用代理模式直接调用方法:

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

✅ 优势:

  • 省去 Callable 或 Lambda 表达式
  • 代码更接近业务调用

8. 私有字段访问

直接访问私有字段进行断言(反射实现):

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

🔧 使用场景:

  • 测试未暴露内部状态
  • 避免添加多余 getter 方法

9. 总结

通过这篇介绍,我们掌握了 Awaitility 的核心能力:

基础用法await().until() 简化异步测试
时间控制:灵活的超时/轮询配置
断言增强:结合 Hamcrest 匹配器
异常处理:优雅忽略预期内异常
高级特性:代理模式和私有字段访问

所有示例代码已上传至 GitHub

一句话总结:用 Awaitility 写异步测试,代码清爽到飞起! 🚀