Як тестувати код з 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, а в яких - реактивне програмування]]