Питання 24 · Розділ 19

Як тестувати код з CompletableFuture

Тестування CompletableFuture зводиться до очікування завершення і перевірки результату:

Мовні версії: English Russian Ukrainian

🟢 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());
}

Типові помилки

  1. Не чекати завершення: ```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, а в яких - реактивне програмування]]