Starter for building WebFlux applications using Spring Framework's Reactive Web support
—
Spring Boot WebFlux provides comprehensive testing support through WebTestClient and reactive testing utilities. This enables full integration testing of reactive web applications with support for mocking, assertions, and test-specific configurations.
public interface WebTestClient {
RequestHeadersUriSpec<?> get();
RequestBodyUriSpec post();
RequestBodyUriSpec put();
RequestBodyUriSpec patch();
RequestHeadersUriSpec<?> delete();
RequestHeadersUriSpec<?> options();
RequestHeadersUriSpec<?> head();
RequestBodyUriSpec method(HttpMethod method);
Builder mutate();
static Builder bindToServer();
static Builder bindToController(Object... controllers);
static Builder bindToApplicationContext(ApplicationContext applicationContext);
static Builder bindToRouterFunction(RouterFunction<?> routerFunction);
static Builder bindToWebHandler(WebHandler webHandler);
static Builder bindToWebServer(int port);
interface Builder {
Builder baseUrl(String baseUrl);
Builder defaultHeader(String header, String... values);
Builder defaultHeaders(Consumer<HttpHeaders> headersConsumer);
Builder defaultCookie(String cookie, String... values);
Builder defaultCookies(Consumer<MultiValueMap<String, String>> cookiesConsumer);
Builder filter(ExchangeFilterFunction filter);
Builder filters(Consumer<List<ExchangeFilterFunction>> filtersConsumer);
Builder clientConnector(ClientHttpConnector connector);
Builder codecs(Consumer<ClientCodecConfigurer> configurer);
Builder exchangeStrategies(ExchangeStrategies strategies);
Builder responseTimeout(Duration timeout);
WebTestClient build();
}
}public interface RequestHeadersUriSpec<S extends RequestHeadersSpec<S>> extends UriSpec<S> {
}
public interface RequestHeadersSpec<S extends RequestHeadersSpec<S>> {
S header(String headerName, String... headerValues);
S headers(Consumer<HttpHeaders> headersConsumer);
S accept(MediaType... acceptableMediaTypes);
S acceptCharset(Charset... acceptableCharsets);
S cookie(String name, String value);
S cookies(Consumer<MultiValueMap<String, String>> cookiesConsumer);
S ifModifiedSince(ZonedDateTime ifModifiedSince);
S ifNoneMatch(String... ifNoneMatches);
ResponseSpec exchange();
}public interface RequestBodyUriSpec extends RequestBodySpec, RequestHeadersUriSpec<RequestBodySpec> {
}
public interface RequestBodySpec extends RequestHeadersSpec<RequestBodySpec> {
RequestHeadersSpec<?> contentLength(long contentLength);
RequestHeadersSpec<?> contentType(MediaType contentType);
RequestHeadersSpec<?> body(BodyInserter<?, ? super ClientHttpRequest> inserter);
<T> RequestHeadersSpec<?> body(Mono<T> body, Class<T> elementClass);
<T> RequestHeadersSpec<?> body(Mono<T> body, ParameterizedTypeReference<T> elementTypeRef);
<T> RequestHeadersSpec<?> body(Flux<T> body, Class<T> elementClass);
<T> RequestHeadersSpec<?> body(Flux<T> body, ParameterizedTypeReference<T> elementTypeRef);
RequestHeadersSpec<?> body(Object body);
RequestHeadersSpec<?> bodyValue(Object body);
}public interface ResponseSpec {
StatusAssertions expectStatus();
HeaderAssertions expectHeader();
JsonPathAssertions expectJsonPath(String expression, Object... args);
XPathAssertions expectXPath(String expression, Object... args, Map<String, String> namespaces);
BodySpec<String, ?> expectBody(String content);
<T> BodySpec<T, ?> expectBody(Class<T> bodyType);
<T> BodySpec<T, ?> expectBody(ParameterizedTypeReference<T> bodyType);
<T> ListBodySpec<T> expectBodyList(Class<T> elementType);
<T> ListBodySpec<T> expectBodyList(ParameterizedTypeReference<T> elementType);
ResponseSpec expectBody();
<T> FluxExchangeResult<T> returnResult(Class<T> elementType);
<T> FluxExchangeResult<T> returnResult(ParameterizedTypeReference<T> elementTypeRef);
}public interface StatusAssertions {
WebTestClient.ResponseSpec isOk();
WebTestClient.ResponseSpec isCreated();
WebTestClient.ResponseSpec isAccepted();
WebTestClient.ResponseSpec isNoContent();
WebTestClient.ResponseSpec isBadRequest();
WebTestClient.ResponseSpec isUnauthorized();
WebTestClient.ResponseSpec isForbidden();
WebTestClient.ResponseSpec isNotFound();
WebTestClient.ResponseSpec isEqualTo(HttpStatus status);
WebTestClient.ResponseSpec isEqualTo(int status);
WebTestClient.ResponseSpec is1xxInformational();
WebTestClient.ResponseSpec is2xxSuccessful();
WebTestClient.ResponseSpec is3xxRedirection();
WebTestClient.ResponseSpec is4xxClientError();
WebTestClient.ResponseSpec is5xxServerError();
}@ExtendWith(SpringExtension.class)
@WebFluxTest(UserController.class)
class UserControllerTest {
@Autowired
private WebTestClient webTestClient;
@MockBean
private UserService userService;
@Test
void shouldGetUser() {
// Given
User user = new User("1", "John Doe", "john@example.com");
when(userService.findById("1")).thenReturn(Mono.just(user));
// When & Then
webTestClient.get()
.uri("/api/users/{id}", "1")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON)
.expectBody(User.class)
.value(u -> {
assertThat(u.getId()).isEqualTo("1");
assertThat(u.getName()).isEqualTo("John Doe");
assertThat(u.getEmail()).isEqualTo("john@example.com");
});
}
@Test
void shouldCreateUser() {
// Given
User newUser = new User(null, "Jane Smith", "jane@example.com");
User savedUser = new User("2", "Jane Smith", "jane@example.com");
when(userService.save(any(User.class))).thenReturn(Mono.just(savedUser));
// When & Then
webTestClient.post()
.uri("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.body(Mono.just(newUser), User.class)
.exchange()
.expectStatus().isCreated()
.expectBody(User.class)
.value(u -> assertThat(u.getId()).isEqualTo("2"));
}
@Test
void shouldGetAllUsers() {
// Given
List<User> users = Arrays.asList(
new User("1", "John Doe", "john@example.com"),
new User("2", "Jane Smith", "jane@example.com")
);
when(userService.findAll()).thenReturn(Flux.fromIterable(users));
// When & Then
webTestClient.get()
.uri("/api/users")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isOk()
.expectBodyList(User.class)
.hasSize(2)
.contains(users.get(0), users.get(1));
}
}@ExtendWith(SpringExtension.class)
@WebFluxTest
class UserRouterTest {
@Autowired
private WebTestClient webTestClient;
@MockBean
private UserHandler userHandler;
@TestConfiguration
static class TestConfig {
@Bean
@Primary
RouterFunction<ServerResponse> testRoutes(UserHandler handler) {
return RouterFunctions.route()
.GET("/api/users/{id}", handler::getUser)
.GET("/api/users", handler::listUsers)
.POST("/api/users", handler::createUser)
.build();
}
}
@Test
void shouldRouteToGetUser() {
// Given
ServerRequest request = MockServerRequest.builder()
.method(HttpMethod.GET)
.uri(URI.create("/api/users/1"))
.pathVariable("id", "1")
.build();
User user = new User("1", "John Doe", "john@example.com");
when(userHandler.getUser(any(ServerRequest.class)))
.thenReturn(ServerResponse.ok().body(Mono.just(user), User.class));
// When & Then
webTestClient.get()
.uri("/api/users/{id}", "1")
.exchange()
.expectStatus().isOk();
}
}@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class UserIntegrationTest {
@Autowired
private WebTestClient webTestClient;
@Autowired
private UserRepository userRepository;
@BeforeEach
void setUp() {
userRepository.deleteAll().block();
}
@Test
void shouldPerformFullUserLifecycle() {
// Create user
User newUser = new User(null, "Integration Test", "integration@example.com");
User createdUser = webTestClient.post()
.uri("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.body(Mono.just(newUser), User.class)
.exchange()
.expectStatus().isCreated()
.expectBody(User.class)
.returnResult()
.getResponseBody();
assertThat(createdUser.getId()).isNotNull();
// Get user
webTestClient.get()
.uri("/api/users/{id}", createdUser.getId())
.exchange()
.expectStatus().isOk()
.expectBody(User.class)
.value(u -> {
assertThat(u.getId()).isEqualTo(createdUser.getId());
assertThat(u.getName()).isEqualTo("Integration Test");
});
// Update user
User updatedUser = new User(createdUser.getId(), "Updated Name", "updated@example.com");
webTestClient.put()
.uri("/api/users/{id}", createdUser.getId())
.contentType(MediaType.APPLICATION_JSON)
.body(Mono.just(updatedUser), User.class)
.exchange()
.expectStatus().isOk()
.expectBody(User.class)
.value(u -> assertThat(u.getName()).isEqualTo("Updated Name"));
// Delete user
webTestClient.delete()
.uri("/api/users/{id}", createdUser.getId())
.exchange()
.expectStatus().isNoContent();
// Verify deletion
webTestClient.get()
.uri("/api/users/{id}", createdUser.getId())
.exchange()
.expectStatus().isNotFound();
}
}@TestConfiguration
public class WebTestClientConfig {
@Bean
@Primary
public WebTestClient customWebTestClient() {
return WebTestClient.bindToServer()
.baseUrl("http://localhost:8080")
.responseTimeout(Duration.ofSeconds(30))
.filter(logRequest())
.filter(logResponse())
.defaultHeader("Accept", MediaType.APPLICATION_JSON_VALUE)
.defaultCookie("SESSION", "test-session")
.build();
}
private ExchangeFilterFunction logRequest() {
return ExchangeFilterFunction.ofRequestProcessor(clientRequest -> {
System.out.println("Request: " + clientRequest.method() + " " + clientRequest.url());
clientRequest.headers().forEach((name, values) ->
values.forEach(value -> System.out.println(name + ": " + value))
);
return Mono.just(clientRequest);
});
}
private ExchangeFilterFunction logResponse() {
return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {
System.out.println("Response: " + clientResponse.statusCode());
return Mono.just(clientResponse);
});
}
}@Test
void shouldTestReactiveStream() {
Flux<String> source = Flux.just("foo", "bar", "baz");
StepVerifier.create(source)
.expectNext("foo")
.expectNext("bar")
.expectNext("baz")
.verifyComplete();
}
@Test
void shouldTestErrorHandling() {
Flux<String> source = Flux.just("foo", "bar")
.concatWith(Flux.error(new RuntimeException("boom")));
StepVerifier.create(source)
.expectNext("foo")
.expectNext("bar")
.expectError(RuntimeException.class)
.verify();
}
@Test
void shouldTestWithVirtualTime() {
StepVerifier.withVirtualTime(() ->
Flux.interval(Duration.ofHours(4)).take(2))
.expectSubscription()
.expectNoEvent(Duration.ofHours(4))
.expectNext(0L)
.thenAwait(Duration.ofHours(4))
.expectNext(1L)
.verifyComplete();
}// Test only WebFlux layer
@WebFluxTest(UserController.class)
class WebFluxLayerTest {
// Only WebFlux components are loaded
}
// Test with custom auto-configuration
@WebFluxTest(
controllers = UserController.class,
excludeAutoConfiguration = SecurityAutoConfiguration.class
)
class CustomWebFluxTest {
// WebFlux with custom configuration
}
// Test specific components
@TestConfiguration
static class TestConfig {
@Bean
@Primary
public UserService mockUserService() {
return Mockito.mock(UserService.class);
}
}Install with Tessl CLI
npx tessl i tessl/maven-org-springframework-boot--spring-boot-starter-webflux