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 для интеграционных тестов контроллеров.