Для всех пользователей Spring в настоящее время я обычно делаю свои интеграционные тесты, где задействовано асинхронное поведение:
Вызвать событие приложения в производственном коде, когда асинхронная задача (например, вызов ввода-вывода) завершена. В большинстве случаев это событие необходимо в любом случае для обработки ответа асинхронной операции в рабочей среде.
После этого события вы можете использовать следующую стратегию в своем тестовом примере:
- Выполнить тестируемую систему
- Прослушайте событие и убедитесь, что событие сработало
- Делай свои утверждения
Чтобы это сломать, сначала нужно запустить некое доменное событие. Я использую UUID здесь, чтобы определить задачу, которая завершена, но вы, конечно, можете использовать что-то еще, если оно уникально.
(Обратите внимание, что следующие фрагменты кода также используют аннотации Lombok, чтобы избавиться от кода котельной пластины)
@RequiredArgsConstructor
class TaskCompletedEvent() {
private final UUID taskId;
// add more fields containing the result of the task if required
}
Сам производственный код обычно выглядит так:
@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));
}
}
Затем я могу использовать Spring, @EventListener
чтобы поймать опубликованное событие в тестовом коде. Слушатель событий немного более вовлечен, потому что он должен обрабатывать два случая потокобезопасным способом:
- Рабочий код работает быстрее, чем тестовый пример, и событие уже сработало до того, как тестовый случай проверит событие, или
- Тестовый случай быстрее, чем производственный код, и тестовый случай должен ждать события.
А CountDownLatch
используется для второго случая, как указано в других ответах здесь. Также обратите внимание, что @Order
аннотация к методу обработчика событий гарантирует, что этот метод обработчика событий вызывается после любых других прослушивателей событий, используемых в производстве.
@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();
}
}
Последний шаг - выполнить тестируемую систему в тестовом примере. Я использую тест SpringBoot с JUnit 5 здесь, но это должно работать одинаково для всех тестов, использующих контекст 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
}
}
Обратите внимание, что в отличие от других ответов здесь, это решение также будет работать, если вы выполняете свои тесты параллельно и несколько потоков одновременно выполняют асинхронный код.