Dla wszystkich użytkowników Spring w ten sposób zwykle przeprowadzam obecnie testy integracyjne, w których występuje zachowanie asynchroniczne:
Uruchom zdarzenie aplikacji w kodzie produkcyjnym, gdy zakończy się zadanie asynchroniczne (takie jak wywołanie We / Wy). W większości przypadków to zdarzenie jest niezbędne do obsługi odpowiedzi operacji asynchronicznej podczas produkcji.
Po wdrożeniu tego zdarzenia możesz następnie użyć następującej strategii w przypadku testowym:
- Uruchom testowany system
- Nasłuchuj wydarzenia i upewnij się, że zostało ono uruchomione
- Czyń swoje twierdzenia
Aby rozwiązać ten problem, musisz najpierw uruchomić jakieś zdarzenie domeny. Korzystam z identyfikatora UUID, aby zidentyfikować ukończone zadanie, ale oczywiście możesz swobodnie korzystać z czegoś innego, o ile jest on unikalny.
(Uwaga: poniższe fragmenty kodu również wykorzystują adnotacje Lombok, aby pozbyć się kodu płyty kotła)
@RequiredArgsConstructor
class TaskCompletedEvent() {
private final UUID taskId;
// add more fields containing the result of the task if required
}
Sam kod produkcyjny zwykle wygląda następująco:
@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));
}
}
Następnie mogę użyć sprężyny, @EventListener
aby złapać opublikowane zdarzenie w kodzie testowym. Nasłuchiwanie zdarzeń jest nieco bardziej zaangażowane, ponieważ musi obsługiwać dwa przypadki w sposób bezpieczny dla wątków:
- Kod produkcyjny jest szybszy niż przypadek testowy, a zdarzenie zostało już uruchomione, zanim przypadek testowy sprawdzi zdarzenie, lub
- Przypadek testowy jest szybszy niż kod produkcyjny i musi on czekać na zdarzenie.
A CountDownLatch
jest używane w drugim przypadku, jak wspomniano w innych odpowiedziach tutaj. Należy również zauważyć, że @Order
adnotacja w metodzie obsługi zdarzeń zapewnia, że ta metoda obsługi zdarzeń zostanie wywołana po każdym innym detektorze zdarzeń używanym w produkcji.
@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();
}
}
Ostatnim krokiem jest wykonanie testowanego systemu w przypadku testowym. Używam tutaj testu SpringBoot z JUnit 5, ale powinien on działać tak samo dla wszystkich testów z kontekstem 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
}
}
Zauważ, że w przeciwieństwie do innych odpowiedzi tutaj, to rozwiązanie będzie również działać, jeśli wykonasz testy równolegle, a wiele wątków jednocześnie wykona kod asynchroniczny.