对于所有 Spring 用户,这就是我现在通常进行集成测试的方式,其中涉及异步行为:
当异步任务(例如 I/O 调用)完成时,在生产代码中触发应用程序事件。在大多数情况下,无论如何都需要此事件来处理生产中异步操作的响应。
有了这个事件,你就可以在你的测试用例中使用以下策略:
- 执行被测系统
- 监听事件并确保事件已触发
- 做你的断言
要对此进行分解,您首先需要触发某种域事件。我在这里使用 UUID 来标识已完成的任务,但您当然可以随意使用其他东西,只要它是唯一的。
(注意,下面的代码sn-ps也使用Lombok注解去掉样板代码)
@RequiredArgsConstructor
class TaskCompletedEvent() {
private final UUID taskId;
// add more fields containing the result of the task if required
}
生产代码本身通常如下所示:
@Component
@RequiredArgsConstructor
class Production {
private final ApplicationEventPublisher eventPublisher;
void doSomeTask(UUID taskId) {
// do something like calling a REST endpoint asynchronously
eventPublisher.publishEvent(new TaskCompletedEvent(taskId));
}
}
然后我可以使用 Spring @EventListener 在测试代码中捕获已发布的事件。事件侦听器涉及更多一点,因为它必须以线程安全的方式处理两种情况:
- 生产代码比测试用例快,并且在测试用例检查事件之前事件已经触发,或者
- 测试用例比生产代码更快,测试用例必须等待事件。
CountDownLatch 用于第二种情况,如此处其他答案中所述。另请注意,事件处理程序方法上的 @Order 注释可确保在生产中使用的任何其他事件侦听器之后调用此事件处理程序方法。
@Component
class TaskCompletionEventListener {
private Map<UUID, CountDownLatch> waitLatches = new ConcurrentHashMap<>();
private List<UUID> eventsReceived = new ArrayList<>();
void waitForCompletion(UUID taskId) {
synchronized (this) {
if (eventAlreadyReceived(taskId)) {
return;
}
checkNobodyIsWaiting(taskId);
createLatch(taskId);
}
waitForEvent(taskId);
}
private void checkNobodyIsWaiting(UUID taskId) {
if (waitLatches.containsKey(taskId)) {
throw new IllegalArgumentException("Only one waiting test per task ID supported, but another test is already waiting for " + taskId + " to complete.");
}
}
private boolean eventAlreadyReceived(UUID taskId) {
return eventsReceived.remove(taskId);
}
private void createLatch(UUID taskId) {
waitLatches.put(taskId, new CountDownLatch(1));
}
@SneakyThrows
private void waitForEvent(UUID taskId) {
var latch = waitLatches.get(taskId);
latch.await();
}
@EventListener
@Order
void eventReceived(TaskCompletedEvent event) {
var taskId = event.getTaskId();
synchronized (this) {
if (isSomebodyWaiting(taskId)) {
notifyWaitingTest(taskId);
} else {
eventsReceived.add(taskId);
}
}
}
private boolean isSomebodyWaiting(UUID taskId) {
return waitLatches.containsKey(taskId);
}
private void notifyWaitingTest(UUID taskId) {
var latch = waitLatches.remove(taskId);
latch.countDown();
}
}
最后一步是在测试用例中执行被测系统。我在这里使用带有 JUnit 5 的 SpringBoot 测试,但这对于使用 Spring 上下文的所有测试应该是一样的。
@SpringBootTest
class ProductionIntegrationTest {
@Autowired
private Production sut;
@Autowired
private TaskCompletionEventListener listener;
@Test
void thatTaskCompletesSuccessfully() {
var taskId = UUID.randomUUID();
sut.doSomeTask(taskId);
listener.waitForCompletion(taskId);
// do some assertions like looking into the DB if value was stored successfully
}
}
请注意,与此处的其他答案相比,如果您并行执行测试并且多个线程同时执行异步代码,此解决方案也将起作用。