Pour tous les utilisateurs de Spring, voici comment je fais habituellement mes tests d'intégration de nos jours, où le comportement asynchrone est impliqué:
Déclenchez un événement d'application dans le code de production lorsqu'une tâche asynchrone (telle qu'un appel d'E / S) est terminée. La plupart du temps, cet événement est de toute façon nécessaire pour gérer la réponse de l'opération asynchrone en production.
Avec cet événement en place, vous pouvez ensuite utiliser la stratégie suivante dans votre scénario de test:
- Exécuter le système sous test
- Écoutez l'événement et assurez-vous que l'événement s'est déclenché
- Faites vos affirmations
Pour décomposer cela, vous aurez d'abord besoin d'une sorte d'événement de domaine à déclencher. J'utilise un UUID ici pour identifier la tâche qui s'est terminée, mais vous êtes bien sûr libre d'utiliser quelque chose d'autre tant qu'il est unique.
(Notez que les extraits de code suivants utilisent également des annotations Lombok pour se débarrasser du code de plaque de chaudière)
@RequiredArgsConstructor
class TaskCompletedEvent() {
private final UUID taskId;
// add more fields containing the result of the task if required
}
Le code de production lui-même ressemble alors généralement à ceci:
@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));
}
}
Je peux ensuite utiliser un ressort @EventListener
pour attraper l'événement publié dans le code de test. L'écouteur d'événements est un peu plus impliqué, car il doit gérer deux cas de manière sécurisée pour les threads:
- Le code de production est plus rapide que le scénario de test et l'événement s'est déjà déclenché avant que le scénario de test ne vérifie l'événement, ou
- Le scénario de test est plus rapide que le code de production et le scénario de test doit attendre l'événement.
A CountDownLatch
est utilisé pour le deuxième cas, comme mentionné dans d'autres réponses ici. Notez également que l' @Order
annotation de la méthode du gestionnaire d'événements garantit que cette méthode du gestionnaire d'événements est appelée après tout autre écouteur d'événements utilisé en production.
@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();
}
}
La dernière étape consiste à exécuter le système testé dans un scénario de test. J'utilise un test SpringBoot avec JUnit 5 ici, mais cela devrait fonctionner de la même manière pour tous les tests utilisant un contexte 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
}
}
Notez que contrairement à d'autres réponses ici, cette solution fonctionnera également si vous exécutez vos tests en parallèle et que plusieurs threads exercent le code asynchrone en même temps.