Как тестировать код с CompletableFuture
Тестирование CompletableFuture сводится к ожиданию завершения и проверке результата:
🟢 Junior Level
Тестирование CompletableFuture сводится к ожиданию завершения и проверке результата:
@Test
void testCompletableFuture() throws Exception {
CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> "Hello");
// Ждём завершения
String result = cf.get(); // или cf.join()
// Проверяем
assertEquals("Hello", result);
}
Для тестов используйте join() — не выбрасывает checked исключения:
String result = cf.join(); // проще чем get()
🟡 Middle Level
Тестирование async кода
1. Прямое тестирование:
@Test
void testAsyncMethod() {
CompletableFuture<String> cf = service.getDataAsync();
// Ждём с таймаутом
String result = cf.get(5, TimeUnit.SECONDS);
assertEquals("expected", result);
}
2. Тестирование с моками:
@Test
void testWithMock() {
// Мокаем async зависимость
when(repository.findByIdAsync(1L))
.thenReturn(CompletableFuture.completedFuture(
new User(1L, "Test")
));
CompletableFuture<User> cf = service.getUserAsync(1L);
User user = cf.join();
assertEquals("Test", user.name());
}
3. Тестирование ошибок:
@Test
void testError() {
when(repository.findByIdAsync(1L))
.thenReturn(CompletableFuture.failedFuture(
new RuntimeException("Not found")
));
CompletableFuture<User> cf = service.getUserAsync(1L);
assertThrows(ExecutionException.class, () -> cf.get());
}
Типичные ошибки
- Не ждать завершения:
```java
// ❌ Тест завершится до выполнения CF
CompletableFuture
cf = service.getDataAsync(); // assertEquals("expected", ???);
// ✅ Ждать String result = cf.join(); assertEquals(“expected”, result);
---
## 🔴 Senior Level
### Testing patterns
**1. Awaitility для асинхронных тестов:**
```java
@Test
void testAsyncChain() {
CompletableFuture<String> cf = service.processAsync();
// Awaitility — библиотека для ожидания завершения асинхронных операций.
// Не путать с StepVerifier из Project Reactor (reactor-test).
await().atMost(5, SECONDS)
.untilAsserted(() -> {
assertTrue(cf.isDone());
assertEquals("expected", cf.join());
});
}
2. Custom Executor for tests:
@Test
void testWithCustomExecutor() {
// Single-threaded executor для детерминированности
ExecutorService executor = Executors.newSingleThreadExecutor();
CompletableFuture<String> cf = service.processAsync(executor);
assertEquals("expected", cf.join());
}
3. Testing race conditions:
@Test
void testConcurrentAccess() throws Exception {
CompletableFuture<Void>[] futures = new CompletableFuture[100];
for (int i = 0; i < 100; i++) {
futures[i] = service.incrementAsync();
}
CompletableFuture.allOf(futures).join();
assertEquals(100, service.getCount());
}
Best Practices
// ✅ join() в тестах (не get)
String result = cf.join();
// ✅ Таймаут
String result = cf.get(5, TimeUnit.SECONDS);
// ✅ completedFuture для моков
when(mock.asyncMethod()).thenReturn(CompletableFuture.completedFuture(result));
// ✅ failedFuture для ошибок
when(mock.asyncMethod()).thenReturn(CompletableFuture.failedFuture(error));
// ❌ Блокировка без таймаута
// ❌ Забывать проверить cf.isDone()
Тестирование с Virtual Threads (Java 21+)
С Virtual Threads многие async-операции можно тестировать как синхронный код:
@Test
void testWithVirtualThreads() {
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
CompletableFuture<String> cf = CompletableFuture.supplyAsync(
() -> service.slowBlockingCall(), executor);
// Virtual Threads позволяют писать обычные синхронные
// assert-проверки без ожидания завершения CompletableFuture.
String result = cf.join();
assertEquals("expected", result);
}
}
🎯 Шпаргалка для интервью
Обязательно знать:
- join() в тестах проще чем get() — не требует try-catch checked исключений
- get(5, TimeUnit.SECONDS) — тест с таймаутом, предотвращает зависание
- completedFuture для моков, failedFuture для тестирования ошибок
- Awaitility для асинхронных тестов с ожиданием завершения
- Custom Executor (single-threaded) для детерминированности тестов
Частые уточняющие вопросы:
- join() или get() в тестах? — join() проще (без try-catch), get(timeout) безопаснее (таймаут)
- Как замокать async метод? — return CompletableFuture.completedFuture(testData)
- Как тестировать async ошибку? — return CompletableFuture.failedFuture(ex) + assertThrows(ExecutionException.class)
- Тестирование race conditions? — CompletableFuture.allOf(N futures).join() + проверка итогового состояния
Красные флаги (НЕ говорить):
- «Без cf.join() тест проходит» — тест завершится до выполнения CF, ложный positive
- «get() без таймаута в тесте — ок» — тест зависнет при ошибке
- «Awaitility и StepVerifier это одно и то же» — Awaitility для general async, StepVerifier для Reactor
Связанные темы:
- [[25. Что делает метод join() и чем он отличается от get()]]
- [[3. Как создать CompletableFuture, который уже завершён с результатом]]
- [[6. Как обрабатывать исключения в цепочке CompletableFuture]]
- [[24. В каких случаях лучше использовать CompletableFuture, а в каких - реактивное программирование]]