Für alle Spring-Benutzer da draußen mache ich heutzutage normalerweise meine Integrationstests, bei denen es um asynchrones Verhalten geht:
Lösen Sie ein Anwendungsereignis im Produktionscode aus, wenn eine asynchrone Aufgabe (z. B. ein E / A-Aufruf) abgeschlossen ist. Meistens ist dieses Ereignis ohnehin erforderlich, um die Reaktion des asynchronen Vorgangs in der Produktion zu verarbeiten.
Mit diesem Ereignis können Sie in Ihrem Testfall die folgende Strategie anwenden:
- Führen Sie das zu testende System aus
- Hören Sie auf das Ereignis und stellen Sie sicher, dass das Ereignis ausgelöst wurde
- Mach deine Behauptungen
Um dies zu beheben, benötigen Sie zunächst eine Art Domain-Ereignis, um ausgelöst zu werden. Ich verwende hier eine UUID, um die abgeschlossene Aufgabe zu identifizieren, aber Sie können natürlich auch etwas anderes verwenden, solange es eindeutig ist.
(Beachten Sie, dass die folgenden Codefragmente auch Lombok- Anmerkungen verwenden, um den Kesselplattencode zu entfernen.)
@RequiredArgsConstructor
class TaskCompletedEvent() {
private final UUID taskId;
// add more fields containing the result of the task if required
}
Der Produktionscode selbst sieht dann normalerweise so aus:
@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));
}
}
Ich kann dann eine Feder verwenden @EventListener
, um das veröffentlichte Ereignis im Testcode abzufangen. Der Ereignis-Listener ist etwas komplizierter, da er zwei Fälle threadsicher behandeln muss:
- Der Produktionscode ist schneller als der Testfall und das Ereignis wurde bereits ausgelöst, bevor der Testfall nach dem Ereignis sucht, oder
- Der Testfall ist schneller als der Produktionscode und der Testfall muss auf das Ereignis warten.
A CountDownLatch
wird für den zweiten Fall verwendet, wie in anderen Antworten hier erwähnt. Beachten Sie außerdem, dass die @Order
Anmerkung zur Ereignishandlermethode sicherstellt, dass diese Ereignishandlermethode nach allen anderen in der Produktion verwendeten Ereignislistenern aufgerufen wird.
@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();
}
}
Der letzte Schritt besteht darin, das zu testende System in einem Testfall auszuführen. Ich verwende hier einen SpringBoot-Test mit JUnit 5, aber dies sollte für alle Tests mit einem Spring-Kontext gleich funktionieren.
@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
}
}
Beachten Sie, dass diese Lösung im Gegensatz zu anderen Antworten hier auch funktioniert, wenn Sie Ihre Tests parallel ausführen und mehrere Threads gleichzeitig den asynchronen Code ausführen.