Bind WebTestClient to the default WebHandler

Previously, if multiple WebHandler beans were present, the auto-config
for WebTestClient fail to identify a suitable candidate as it expects
to only have such a bean.

This commit updates the logic to look for a well-known bean name that
WebFlux uses, and clarify the exception message to state that a bean
with a given name is expected to be found.

The exception message has been further refined to mention that, if
such a bean is not present, then a MockMVc-compatible ApplicationContext
should be available (i.e. WebApplicationContext).

Closes gh-47617
This commit is contained in:
Stéphane Nicoll
2025-10-14 11:00:49 +02:00
parent c4d3583791
commit 879b7e6cce
4 changed files with 163 additions and 62 deletions
@@ -18,7 +18,6 @@ package org.springframework.boot.webtestclient;
import java.util.List;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
@@ -40,7 +39,7 @@ import org.springframework.test.web.servlet.client.MockMvcWebTestClient;
import org.springframework.util.ClassUtils;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.server.WebHandler;
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
/**
* Auto-configuration for {@link WebTestClient}.
@@ -79,31 +78,22 @@ public final class WebTestClientAutoConfiguration {
if (baseUrl != null) {
return WebTestClient.bindToServer().uriBuilderFactory(BaseUrlUriBuilderFactory.get(baseUrl));
}
if (hasBean(applicationContext, WebHandler.class)) {
if (applicationContext.containsBean(WebHttpHandlerBuilder.WEB_HANDLER_BEAN_NAME)) {
MockServerSpec<?> spec = WebTestClient.bindToApplicationContext(applicationContext);
configurers.forEach(spec::apply);
return spec.configureClient();
}
if (ClassUtils.isPresent(WEB_APPLICATION_CONTEXT_CLASS, applicationContext.getClassLoader())) {
if (hasBean(applicationContext, MockMvc.class)) {
return MockMvcWebTestClient.bindTo(applicationContext.getBean(MockMvc.class));
MockMvc mockMvc = applicationContext.getBeanProvider(MockMvc.class).getIfUnique();
if (mockMvc != null) {
return MockMvcWebTestClient.bindTo(mockMvc);
}
if (applicationContext instanceof WebApplicationContext webApplicationContext) {
return MockMvcWebTestClient.bindToApplicationContext(webApplicationContext).configureClient();
}
}
throw new IllegalStateException(
"Mock WebTestClient support requires a WebHandler or MockMvc bean and neither was present");
}
private boolean hasBean(ApplicationContext applicationContext, Class<?> type) {
try {
applicationContext.getBean(type);
return true;
}
catch (NoSuchBeanDefinitionException ex) {
return false;
}
"Mock WebTestClient support requires a WebHandler (named 'webHandler') bean or a WebApplicationContext and neither was present");
}
}
@@ -71,6 +71,31 @@ class WebTestClientAutoConfigurationTests {
});
}
@Test
void shouldFailWhenDefaultWebHandlerIsNotAvailable() {
this.contextRunner.withBean("myWebHandler", WebHandler.class, () -> mock(WebHandler.class)).run((context) -> {
assertThat(context).hasFailed();
assertThat(context).getFailure()
.rootCause()
.isInstanceOf(RuntimeException.class)
.hasMessageStartingWith("Mock WebTestClient support requires")
.hasMessageContaining("WebHandler");
});
}
@Test
void shouldFailWithNeitherDefaultWebHandlerNorWebApplicationContext() {
ClassLoader parentClassLoader = Thread.currentThread().getContextClassLoader();
this.contextRunner.withClassLoader(new FilteredClassLoader(parentClassLoader, WebApplicationContext.class))
.run((context) -> {
assertThat(context).getFailure()
.rootCause()
.isInstanceOf(RuntimeException.class)
.hasMessageStartingWith("Mock WebTestClient support requires")
.hasMessageContaining("WebApplicationContext");
});
}
@Test
@WithResource(name = "META-INF/spring.factories", content = """
org.springframework.boot.test.http.server.BaseUrlProvider=\
@@ -0,0 +1,114 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package smoketest.webflux;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.webtestclient.AutoConfigureWebTestClient;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.test.web.reactive.server.WebTestClient;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Basic integration tests for WebFlux application.
*
* @author Brian Clozel
*/
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = "server.error.include-message=always")
@AutoConfigureWebTestClient
class SampleWebFluxApplicationIntegrationTests {
@Autowired
private WebTestClient webClient;
@Test
void testWelcome() {
this.webClient.get()
.uri("/")
.accept(MediaType.TEXT_PLAIN)
.exchange()
.expectBody(String.class)
.isEqualTo("Hello World");
}
@Test
void testEcho() {
this.webClient.post()
.uri("/echo")
.contentType(MediaType.TEXT_PLAIN)
.accept(MediaType.TEXT_PLAIN)
.body(Mono.just("Hello WebFlux!"), String.class)
.exchange()
.expectBody(String.class)
.isEqualTo("Hello WebFlux!");
}
@Test
void testActuatorStatus() {
this.webClient.get()
.uri("/actuator/health")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus()
.isOk()
.expectBody()
.json("{\"status\":\"UP\"}");
}
@Test
void templated404ErrorPage() {
this.webClient.get()
.uri("/404")
.accept(MediaType.TEXT_HTML)
.exchange()
.expectStatus()
.isNotFound()
.expectBody(String.class)
.value((body) -> assertThat(body).isEqualToNormalizingNewlines("404 page\n"));
}
@Test
void templated4xxErrorPage() {
this.webClient.get()
.uri("/bad-request")
.accept(MediaType.TEXT_HTML)
.exchange()
.expectStatus()
.isBadRequest()
.expectBody(String.class)
.value((body) -> assertThat(body).isEqualToNormalizingNewlines("4xx page\n"));
}
@Test
void htmlErrorPage() {
this.webClient.get()
.uri("/five-hundred")
.accept(MediaType.TEXT_HTML)
.exchange()
.expectStatus()
.isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR)
.expectBody(String.class)
.value((body) -> assertThat(body).contains("status: 500").contains("message: Expected!"));
}
}
@@ -16,13 +16,15 @@
package smoketest.webflux;
import java.util.Map;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.webtestclient.AutoConfigureWebTestClient;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.test.web.reactive.server.WebTestClient;
@@ -30,27 +32,21 @@ import org.springframework.test.web.reactive.server.WebTestClient;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Basic integration tests for WebFlux application.
* Basic tests for a WebFlux application, configuring the {@link WebTestClient} to test
* without a running server.
*
* @author Brian Clozel
* @author Stephane Nicoll
*/
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = "server.error.include-message=always")
@SpringBootTest
@AutoConfigureWebTestClient
class SampleWebFluxApplicationTests {
private static final ParameterizedTypeReference<Map<String, Object>> MAP_TYPE = new ParameterizedTypeReference<Map<String, Object>>() {
};
@Autowired
private WebTestClient webClient;
@Test
void testWelcome() {
this.webClient.get()
.uri("/")
.accept(MediaType.TEXT_PLAIN)
.exchange()
.expectBody(String.class)
.isEqualTo("Hello World");
}
@Test
void testEcho() {
this.webClient.post()
@@ -64,51 +60,27 @@ class SampleWebFluxApplicationTests {
}
@Test
void testActuatorStatus() {
void testBadRequest() {
this.webClient.get()
.uri("/actuator/health")
.uri("/bad-request")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus()
.isOk()
.expectBody()
.json("{\"status\":\"UP\"}");
}
@Test
void templated404ErrorPage() {
this.webClient.get()
.uri("/404")
.accept(MediaType.TEXT_HTML)
.exchange()
.expectStatus()
.isNotFound()
.expectBody(String.class)
.value((body) -> assertThat(body).isEqualToNormalizingNewlines("404 page\n"));
}
@Test
void templated4xxErrorPage() {
this.webClient.get()
.uri("/bad-request")
.accept(MediaType.TEXT_HTML)
.exchange()
.expectStatus()
.isBadRequest()
.expectBody(String.class)
.value((body) -> assertThat(body).isEqualToNormalizingNewlines("4xx page\n"));
.expectBody(MAP_TYPE)
.value((content) -> assertThat(content).containsEntry("path", "/bad-request"));
}
@Test
void htmlErrorPage() {
void testServerError() {
this.webClient.get()
.uri("/five-hundred")
.accept(MediaType.TEXT_HTML)
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus()
.isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR)
.expectBody(String.class)
.value((body) -> assertThat(body).contains("status: 500").contains("message: Expected!"));
.expectBody(MAP_TYPE)
.value((content) -> assertThat(content).containsEntry("path", "/five-hundred"));
}
}