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 写异步测试,代码清爽到飞起! 🚀