Para todos los usuarios de Spring, así es como suelo hacer mis pruebas de integración hoy en día, donde está involucrado el comportamiento asíncrono:
Active un evento de aplicación en el código de producción, cuando una tarea asíncrona (como una llamada de E / S) haya finalizado. La mayoría de las veces este evento es necesario de todos modos para manejar la respuesta de la operación asíncrona en producción.
Con este evento en su lugar, puede utilizar la siguiente estrategia en su caso de prueba:
- Ejecute el sistema bajo prueba
- Escuche el evento y asegúrese de que se haya disparado.
- Haz tus afirmaciones
Para desglosar esto, primero necesitará algún tipo de evento de dominio para disparar. Estoy usando un UUID aquí para identificar la tarea que se ha completado, pero por supuesto eres libre de usar otra cosa siempre que sea única.
(Tenga en cuenta que los siguientes fragmentos de código también usan anotaciones de Lombok para deshacerse del código de la placa de la caldera)
@RequiredArgsConstructor
class TaskCompletedEvent() {
private final UUID taskId;
// add more fields containing the result of the task if required
}
El código de producción en sí normalmente se ve así:
@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));
}
}
Entonces puedo usar un Spring @EventListener
para capturar el evento publicado en el código de prueba. El oyente de eventos está un poco más involucrado, porque tiene que manejar dos casos de manera segura para subprocesos:
- El código de producción es más rápido que el caso de prueba y el evento ya se disparó antes de que el caso de prueba verifique el evento, o
- El caso de prueba es más rápido que el código de producción y el caso de prueba tiene que esperar el evento.
A CountDownLatch
se usa para el segundo caso como se menciona en otras respuestas aquí. También tenga en cuenta que la @Order
anotación en el método de controlador de eventos asegura que este método de controlador de eventos se llama después de cualquier otro detector de eventos utilizado en producción.
@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();
}
}
El último paso es ejecutar el sistema bajo prueba en un caso de prueba. Estoy usando una prueba SpringBoot con JUnit 5 aquí, pero esto debería funcionar igual para todas las pruebas que usan un contexto 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
}
}
Tenga en cuenta que, a diferencia de otras respuestas aquí, esta solución también funcionará si ejecuta sus pruebas en paralelo y múltiples hilos ejercen el código asíncrono al mismo tiempo.