Gymterview
middle

Как тестировать реактивный код?

Реактивный код тестируется через StepVerifier — инструмент из reactor-test, который подписывается на Mono/Flux и пошагово проверяет сигналы. Нельзя просто вызвать метод и проверить результат, потому что Mono и Flux ленивы.

StepVerifier — основной инструмент

Пример
// Тестирование Mono
@Test
void testFindById() {
    Mono<User> userMono = userService.findById(1L);

    StepVerifier.create(userMono)
        .expectNextMatches(user -> user.getName().equals("John"))
        .verifyComplete();
}

// Тестирование Flux
@Test
void testFindAll() {
    Flux<User> usersFlux = userService.findAll();

    StepVerifier.create(usersFlux)
        .expectNextCount(3)
        .verifyComplete();
}

// Тестирование ошибок
@Test
void testError() {
    Mono<User> errorMono = userService.findById(-1L);

    StepVerifier.create(errorMono)
        .expectError(NotFoundException.class)
        .verify();
}

Тестирование с виртуальным временем

Пример
@Test
void testWithVirtualTime() {
    StepVerifier.withVirtualTime(() ->
            Flux.interval(Duration.ofHours(1)).take(3))
        .expectSubscription()
        .thenAwait(Duration.ofHours(3))
        .expectNext(0L, 1L, 2L)
        .verifyComplete();
}
TestPublisher — управляемый источник для тестов
@Test
void testWithTestPublisher() {
    TestPublisher<String> testPublisher = TestPublisher.create();

    Flux<String> flux = testPublisher.flux()
        .map(String::toUpperCase);

    StepVerifier.create(flux)
        .then(() -> testPublisher.emit("hello", "world"))
        .expectNext("HELLO", "WORLD")
        .verifyComplete();
}

// Симуляция ошибки
@Test
void testErrorWithTestPublisher() {
    TestPublisher<String> testPublisher = TestPublisher.create();

    StepVerifier.create(testPublisher.flux())
        .then(() -> {
            testPublisher.next("data");
            testPublisher.error(new RuntimeException("test error"));
        })
        .expectNext("data")
        .expectError(RuntimeException.class)
        .verify();
}
WebTestClient — тестирование WebFlux-контроллеров
@WebFluxTest(UserController.class)
class UserControllerTest {

    @Autowired
    private WebTestClient webTestClient;

    @MockitoBean
    private UserService userService;

    @Test
    void shouldReturnUser() {
        when(userService.findById(1L)).thenReturn(Mono.just(new User(1L, "John")));

        webTestClient.get().uri("/api/users/1")
            .exchange()
            .expectStatus().isOk()
            .expectBody(User.class)
            .value(user -> assertThat(user.getName()).isEqualTo("John"));
    }

    @Test
    void shouldReturnAllUsers() {
        when(userService.findAll()).thenReturn(Flux.just(
            new User(1L, "John"), new User(2L, "Jane")));

        webTestClient.get().uri("/api/users")
            .exchange()
            .expectStatus().isOk()
            .expectBodyList(User.class)
            .hasSize(2);
    }
}

Частые ошибки

  • Забыть вызвать .verify() — StepVerifier не подпишется, тест будет «зелёным» без проверки
  • Использовать .block() вместо StepVerifier — теряется возможность проверки сигналов и порядка
  • Не использовать withVirtualTime для interval/delay — тест будет ждать реальное время
  • Моки, возвращающие null — нужно возвращать Mono.empty() или Flux.empty(), а не null

Как используется в 2026

  • StepVerifier — стабильный стандарт де-факто
  • WebTestClient используется не только для WebFlux, но и для интеграционных тестов Spring MVC
  • Testcontainers + R2DBC — стандартная связка для интеграционных тестов реактивных репозиториев
  • AssertJ интеграция через StepVerifier + reactor-test

На собеседовании: минимум — знать StepVerifier и его основные методы (expectNext, verifyComplete, expectError). Частая ошибка — не упомянуть withVirtualTime для тестирования операторов с задержкой и WebTestClient для интеграционных тестов контроллеров.