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