Per tutti gli utenti Spring là fuori, questo è il modo in cui di solito faccio i miei test di integrazione al giorno d'oggi, in cui è coinvolto il comportamento asincrono:
Attiva un evento dell'applicazione nel codice di produzione al termine di un'attività asincrona (come una chiamata I / O). Il più delle volte questo evento è comunque necessario per gestire la risposta dell'operazione asincrona in produzione.
Con questo evento attivo, puoi utilizzare la seguente strategia nel tuo caso di test:
- Eseguire il sistema in prova
- Ascolta l'evento e assicurati che l'evento sia stato attivato
- Fai le tue affermazioni
Per scomporlo, devi prima attivare un evento di dominio. Sto usando un UUID qui per identificare l'attività che è stata completata, ma ovviamente sei libero di usare qualcos'altro purché unico.
(Nota che i seguenti frammenti di codice usano anche le annotazioni Lombok per eliminare il codice della piastra della caldaia)
@RequiredArgsConstructor
class TaskCompletedEvent() {
private final UUID taskId;
// add more fields containing the result of the task if required
}
Il codice di produzione stesso si presenta quindi in questo modo:
@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));
}
}
Posso quindi utilizzare una primavera @EventListener
per catturare l'evento pubblicato nel codice di prova. Il listener di eventi è un po 'più coinvolto, perché deve gestire due casi in modo sicuro thread:
- Il codice di produzione è più veloce del test case e l'evento è già stato attivato prima che il test case controlli l'evento, oppure
- Il test case è più veloce del codice di produzione e il test case deve attendere l'evento.
A CountDownLatch
viene utilizzato per il secondo caso, come indicato in altre risposte qui. Si noti inoltre che l' @Order
annotazione sul metodo del gestore eventi assicura che questo metodo del gestore eventi venga chiamato dopo qualsiasi altro listener di eventi utilizzato nella produzione.
@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();
}
}
L'ultimo passo è eseguire il sistema sotto test in un caso di test. Sto usando un test SpringBoot con JUnit 5 qui, ma questo dovrebbe funzionare allo stesso modo per tutti i test che utilizzano un contesto 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
}
}
Nota che, contrariamente alle altre risposte qui, questa soluzione funzionerà anche se esegui i test in parallelo e più thread esercitano il codice asincrono contemporaneamente.