Para todos os usuários do Spring, é assim que eu costumo fazer meus testes de integração hoje em dia, onde o comportamento assíncrono está envolvido:
Dispare um evento de aplicativo no código de produção, quando uma tarefa assíncrona (como uma chamada de E / S) for concluída. Na maioria das vezes, esse evento é necessário para lidar com a resposta da operação assíncrona na produção.
Com esse evento, você pode usar a seguinte estratégia no seu caso de teste:
- Execute o sistema em teste
- Ouça o evento e verifique se o evento foi disparado
- Faça suas afirmações
Para quebrar isso, primeiro você precisará de algum tipo de evento de domínio para disparar. Estou usando um UUID aqui para identificar a tarefa que foi concluída, mas é claro que você pode usar outra coisa, desde que ela seja única.
(Observe que os seguintes trechos de código também usam anotações Lombok para se livrar do código da placa da caldeira)
@RequiredArgsConstructor
class TaskCompletedEvent() {
private final UUID taskId;
// add more fields containing the result of the task if required
}
O código de produção em si normalmente fica assim:
@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 usar um Spring @EventListener
para capturar o evento publicado no código de teste. O ouvinte de evento é um pouco mais envolvido, porque precisa lidar com dois casos de uma maneira segura para threads:
- O código de produção é mais rápido que o caso de teste e o evento já foi acionado antes que o caso de teste verifique o evento ou
- O caso de teste é mais rápido que o código de produção e o caso de teste precisa aguardar o evento.
A CountDownLatch
é usado para o segundo caso, como mencionado em outras respostas aqui. Observe também que a @Order
anotação no método manipulador de eventos garante que esse método manipulador de eventos seja chamado após qualquer outro ouvinte de evento usado na produção.
@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();
}
}
O último passo é executar o sistema em teste em um caso de teste. Estou usando um teste SpringBoot com o JUnit 5 aqui, mas isso deve funcionar da mesma forma para todos os testes usando um 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
}
}
Observe que, ao contrário de outras respostas aqui, esta solução também funcionará se você executar seus testes em paralelo e vários segmentos exercitarem o código assíncrono ao mesmo tempo.