Add support for RestTestClient
Build and Deploy Snapshot / Build and Deploy Snapshot (push) Waiting to run Details
Build and Deploy Snapshot / Trigger Docs Build (push) Blocked by required conditions Details
Build and Deploy Snapshot / Verify (push) Blocked by required conditions Details
CI / ${{ matrix.os.name}} | Java ${{ matrix.java.version}} (map[toolchain:false version:24], map[id:${{ vars.UBUNTU_MEDIUM || 'ubuntu-latest' }} name:Linux]) (push) Waiting to run Details
CI / ${{ matrix.os.name}} | Java ${{ matrix.java.version}} (map[toolchain:false version:24], map[id:windows-latest name:Windows]) (push) Waiting to run Details
CI / ${{ matrix.os.name}} | Java ${{ matrix.java.version}} (map[toolchain:true version:17], map[id:${{ vars.UBUNTU_MEDIUM || 'ubuntu-latest' }} name:Linux]) (push) Waiting to run Details
CI / ${{ matrix.os.name}} | Java ${{ matrix.java.version}} (map[toolchain:true version:17], map[id:windows-latest name:Windows]) (push) Waiting to run Details
CI / ${{ matrix.os.name}} | Java ${{ matrix.java.version}} (map[toolchain:true version:21], map[id:${{ vars.UBUNTU_MEDIUM || 'ubuntu-latest' }} name:Linux]) (push) Waiting to run Details
CI / ${{ matrix.os.name}} | Java ${{ matrix.java.version}} (map[toolchain:true version:21], map[id:windows-latest name:Windows]) (push) Waiting to run Details
CI / ${{ matrix.os.name}} | Java ${{ matrix.java.version}} (map[toolchain:true version:25], map[id:${{ vars.UBUNTU_MEDIUM || 'ubuntu-latest' }} name:Linux]) (push) Waiting to run Details
CI / ${{ matrix.os.name}} | Java ${{ matrix.java.version}} (map[toolchain:true version:25], map[id:windows-latest name:Windows]) (push) Waiting to run Details
Run CodeQL Analysis / run-analysis (push) Waiting to run Details
Run System Tests / Java ${{ matrix.java.version}} (map[toolchain:true version:17]) (push) Waiting to run Details
Run System Tests / Java ${{ matrix.java.version}} (map[toolchain:true version:21]) (push) Waiting to run Details

This commit adds support for RestTestClient for MockMvc and integration
tests.

Closes gh-47335
This commit is contained in:
Stéphane Nicoll 2025-09-30 15:34:28 +02:00
parent e2ba4dad2a
commit 787840735b
19 changed files with 854 additions and 7 deletions

View File

@ -141,10 +141,11 @@ include-code::MyApplicationArgumentTests[]
By default, javadoc:org.springframework.boot.test.context.SpringBootTest[format=annotation] does not start the server but instead sets up a mock environment for testing web endpoints.
With Spring MVC, we can query our web endpoints using {url-spring-framework-docs}/testing/mockmvc.html[`MockMvc`].
Three integrations are available:
The following integrations are available:
* The regular {url-spring-framework-docs}/testing/mockmvc/hamcrest.html[`MockMvc`] that uses Hamcrest.
* {url-spring-framework-docs}/testing/mockmvc/assertj.html[`MockMvcTester`] that wraps javadoc:org.springframework.test.web.servlet.MockMvc[] and uses AssertJ.
* {url-spring-framework-docs}/testing/resttestclient.html[`RestTestClient`] where javadoc:org.springframework.test.web.servlet.MockMvc[] is plugged in as the server to handle requests with.
* {url-spring-framework-docs}/testing/webtestclient.html[`WebTestClient`] where javadoc:org.springframework.test.web.servlet.MockMvc[] is plugged in as the server to handle requests with.
The following example showcases the available integrations:
@ -176,19 +177,32 @@ If you need to start a full running server, we recommend that you use random por
If you use `@SpringBootTest(webEnvironment=WebEnvironment.RANDOM_PORT)`, an available port is picked at random each time your test runs.
The javadoc:org.springframework.boot.test.web.server.LocalServerPort[format=annotation] annotation can be used to xref:how-to:webserver.adoc#howto.webserver.discover-port[inject the actual port used] into your test.
For convenience, tests that need to make REST calls to the started server can additionally autowire a {url-spring-framework-docs}/testing/webtestclient.html[`WebTestClient`], which resolves relative links to the running server and comes with a dedicated API for verifying responses, as shown in the following example:
For convenience, tests that need to make REST calls to the started server can additionally autowire a
{url-spring-framework-docs}/testing/resttestclient.html[`RestTestClient`] which resolves relative links to the running server and comes with a dedicated API for verifying responses, as shown in the following example:
include-code::MyRandomPortRestTestClientTests[]
If you have `spring-webflux` on the classpath, you can also autowire a {url-spring-framework-docs}/testing/webtestclient.html[`WebTestClient`] that provides a similar API:
include-code::MyRandomPortWebTestClientTests[]
TIP: javadoc:org.springframework.test.web.reactive.server.WebTestClient[] can also used with a xref:testing/spring-boot-applications.adoc#testing.spring-boot-applications.with-mock-environment[mock environment], removing the need for a running server, by annotating your test class with javadoc:org.springframework.boot.webflux.test.autoconfigure.AutoConfigureWebTestClient[format=annotation] from `spring-boot-webflux-test`.
This setup requires `spring-webflux` on the classpath.
If you can not or will not add webflux, the `spring-boot-web-server-test` modules provides a javadoc:org.springframework.boot.web.server.test.client.TestRestTemplate[] facility:
The `spring-boot-web-server-test` modules also provides a javadoc:org.springframework.boot.web.server.test.client.TestRestTemplate[] facility:
include-code::MyRandomPortTestRestTemplateTests[]
[[testing.spring-boot-applications.customizing-rest-test-client]]
== Customizing RestTestClient
To customize the javadoc:org.springframework.test.web.servlet.client.RestTestClient[] bean, configure a javadoc:org.springframework.boot.web.server.test.client.RestTestClientBuilderCustomizer[] bean.
Any such beans are called with the javadoc:org.springframework.test.web.servlet.client.RestTestClient$Builder[] that is used to create the javadoc:org.springframework.test.web.servlet.client.RestTestClient[].
[[testing.spring-boot-applications.customizing-web-test-client]]
== Customizing WebTestClient

View File

@ -48,8 +48,9 @@ In either case, the template is fault tolerant.
This means that it behaves in a test-friendly way by not throwing exceptions on 4xx and 5xx errors.
Instead, such errors can be detected through the returned javadoc:org.springframework.http.ResponseEntity[] and its status code.
TIP: Spring Framework 5.0 provides a new javadoc:org.springframework.test.web.reactive.server.WebTestClient[] that works for xref:testing/spring-boot-applications.adoc#testing.spring-boot-applications.spring-webflux-tests[WebFlux integration tests] and both xref:testing/spring-boot-applications.adoc#testing.spring-boot-applications.with-running-server[WebFlux and MVC end-to-end testing].
It provides a fluent API for assertions, unlike javadoc:org.springframework.boot.test.web.client.TestRestTemplate[].
If you need fluent API for assertions, consider using javadoc:org.springframework.test.web.servlet.client.RestTestClient[] that works with xref:testing/spring-boot-applications.adoc#testing.spring-boot-applications.with-mock-environment[mock environments] and xref:testing/spring-boot-applications.adoc#testing.spring-boot-applications.with-running-server[end-to-end tests].
If you are using Spring WebFlux, consider the javadoc:org.springframework.test.web.reactive.server.WebTestClient[] that provides a similar API and works with xref:testing/spring-boot-applications.adoc#testing.spring-boot-applications.with-mock-environment[mock environments], xref:testing/spring-boot-applications.adoc#testing.spring-boot-applications.spring-webflux-tests[WebFlux integration tests], and xref:testing/spring-boot-applications.adoc#testing.spring-boot-applications.with-running-server[end-to-end tests].
It is recommended, but not mandatory, to use the Apache HTTP Client (version 5.1 or better).
If you have that on your classpath, the javadoc:org.springframework.boot.test.web.client.TestRestTemplate[] responds by configuring the client appropriately.

View File

@ -24,6 +24,7 @@ import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.assertj.MockMvcTester;
import org.springframework.test.web.servlet.client.RestTestClient;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
@ -45,6 +46,17 @@ class MyMockMvcTests {
assertThat(mvc.get().uri("/")).hasStatusOk().hasBodyTextEqualTo("Hello World");
}
@Test
void testWithRestTestClient(@Autowired RestTestClient webClient) {
// @formatter:off
webClient
.get().uri("/")
.exchange()
.expectStatus().isOk()
.expectBody(String.class).isEqualTo("Hello World");
// @formatter:on
}
// If Spring WebFlux is on the classpath, you can drive MVC tests with a WebTestClient
@Test
void testWithWebTestClient(@Autowired WebTestClient webClient) {

View File

@ -0,0 +1,40 @@
/*
* 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 org.springframework.boot.docs.testing.springbootapplications.withrunningserver;
import org.junit.jupiter.api.Test;
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.test.web.servlet.client.RestTestClient;
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class MyRandomPortRestTestClientTests {
@Test
void exampleTest(@Autowired RestTestClient webClient) {
// @formatter:off
webClient
.get().uri("/")
.exchange()
.expectStatus().isOk()
.expectBody(String.class).isEqualTo("Hello World");
// @formatter:on
}
}

View File

@ -31,7 +31,12 @@ import org.springframework.web.util.UriTemplateHandler;
*/
public class RootUriBuilderFactory extends RootUriTemplateHandler implements UriBuilderFactory {
RootUriBuilderFactory(String rootUri, UriTemplateHandler delegate) {
/**
* Create an instance with the root URI to use.
* @param rootUri the root URI
* @param delegate the {@link UriTemplateHandler} to delegate to
*/
public RootUriBuilderFactory(String rootUri, UriTemplateHandler delegate) {
super(rootUri, delegate);
}

View File

@ -0,0 +1,38 @@
/*
* 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 org.springframework.boot.web.server.test.client;
import org.springframework.test.web.servlet.client.RestTestClient;
/**
* A customizer that can be implemented by beans wishing to customize the
* {@link RestTestClient.Builder} to fine-tine its auto-configuration before a
* {@link RestTestClient} is created.
*
* @author Stephane Nicoll
* @since 4.0.0
*/
@FunctionalInterface
public interface RestTestClientBuilderCustomizer {
/**
* Customize the given {@link RestTestClient.Builder Builder}.
* @param builder the builder
*/
void customize(RestTestClient.Builder<?> builder);
}

View File

@ -0,0 +1,194 @@
/*
* 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 org.springframework.boot.web.server.test.client;
import org.jspecify.annotations.Nullable;
import org.springframework.aot.AotDetector;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.boot.restclient.RootUriBuilderFactory;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.server.reactive.AbstractReactiveWebServerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.ConfigurationClassPostProcessor;
import org.springframework.core.Ordered;
import org.springframework.test.context.ContextCustomizer;
import org.springframework.test.context.MergedContextConfiguration;
import org.springframework.test.context.TestContextAnnotationUtils;
import org.springframework.test.web.servlet.client.RestTestClient;
import org.springframework.util.Assert;
/**
* {@link ContextCustomizer} for {@link RestTestClient}.
*
* @author Stephane Nicoll
*/
class RestTestClientContextCustomizer implements ContextCustomizer {
@Override
public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) {
if (AotDetector.useGeneratedArtifacts()) {
return;
}
SpringBootTest springBootTest = TestContextAnnotationUtils.findMergedAnnotation(mergedConfig.getTestClass(),
SpringBootTest.class);
Assert.state(springBootTest != null, "'springBootTest' must not be null");
if (springBootTest.webEnvironment().isEmbedded()) {
registerRestTestClient(context);
}
}
private void registerRestTestClient(ConfigurableApplicationContext context) {
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
if (beanFactory instanceof BeanDefinitionRegistry registry) {
registerRestTestClient(registry);
}
}
private void registerRestTestClient(BeanDefinitionRegistry registry) {
RootBeanDefinition definition = new RootBeanDefinition(RestTestClientRegistrar.class);
definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
registry.registerBeanDefinition(RestTestClientRegistrar.class.getName(), definition);
}
@Override
public boolean equals(@Nullable Object obj) {
return (obj != null) && (obj.getClass() == getClass());
}
@Override
public int hashCode() {
return getClass().hashCode();
}
/**
* {@link BeanDefinitionRegistryPostProcessor} that runs after the
* {@link ConfigurationClassPostProcessor} and add a {@link RestTestClientFactory}
* bean definition when a {@link RestTestClient} hasn't already been registered.
*/
static class RestTestClientRegistrar implements BeanDefinitionRegistryPostProcessor, Ordered, BeanFactoryAware {
@SuppressWarnings("NullAway.Init")
private BeanFactory beanFactory;
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
if (AotDetector.useGeneratedArtifacts()) {
return;
}
if (BeanFactoryUtils.beanNamesForTypeIncludingAncestors((ListableBeanFactory) this.beanFactory,
RestTestClient.class, false, false).length == 0) {
registry.registerBeanDefinition(RestTestClient.class.getName(),
new RootBeanDefinition(RestTestClientFactory.class));
}
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
}
}
/**
* {@link FactoryBean} used to create and configure a {@link RestTestClient}.
*/
public static class RestTestClientFactory implements FactoryBean<RestTestClient>, ApplicationContextAware {
@SuppressWarnings("NullAway.Init")
private ApplicationContext applicationContext;
private @Nullable RestTestClient object;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@Override
public boolean isSingleton() {
return true;
}
@Override
public Class<?> getObjectType() {
return RestTestClient.class;
}
@Override
public RestTestClient getObject() {
if (this.object == null) {
this.object = createRestTestClient();
}
return this.object;
}
private RestTestClient createRestTestClient() {
boolean sslEnabled = isSslEnabled(this.applicationContext);
LocalHostUriTemplateHandler handler = new LocalHostUriTemplateHandler(
this.applicationContext.getEnvironment(), sslEnabled ? "https" : "http");
RestTestClient.Builder<?> builder = RestTestClient.bindToServer();
customizeRestTestClientBuilder(builder, this.applicationContext);
return builder.uriBuilderFactory(new RootUriBuilderFactory(handler.getRootUri(), handler)).build();
}
private boolean isSslEnabled(ApplicationContext context) {
try {
AbstractReactiveWebServerFactory webServerFactory = context
.getBean(AbstractReactiveWebServerFactory.class);
return webServerFactory.getSsl() != null && webServerFactory.getSsl().isEnabled();
}
catch (NoSuchBeanDefinitionException ex) {
return false;
}
}
private void customizeRestTestClientBuilder(RestTestClient.Builder<?> clientBuilder,
ApplicationContext context) {
for (RestTestClientBuilderCustomizer customizer : context
.getBeansOfType(RestTestClientBuilderCustomizer.class)
.values()) {
customizer.customize(clientBuilder);
}
}
}
}

View File

@ -0,0 +1,52 @@
/*
* 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 org.springframework.boot.web.server.test.client;
import java.util.List;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfigurationAttributes;
import org.springframework.test.context.ContextCustomizer;
import org.springframework.test.context.ContextCustomizerFactory;
import org.springframework.test.context.TestContextAnnotationUtils;
import org.springframework.util.ClassUtils;
/**
* {@link ContextCustomizerFactory} for {@code RestTestClient}.
*
* @author Stephane Nicoll
*/
class RestTestClientContextCustomizerFactory implements ContextCustomizerFactory {
private static final boolean restClientPresent;
static {
ClassLoader loader = RestTestClientContextCustomizerFactory.class.getClassLoader();
restClientPresent = ClassUtils.isPresent("org.springframework.web.client.RestClient", loader);
}
@Override
public @Nullable ContextCustomizer createContextCustomizer(Class<?> testClass,
List<ContextConfigurationAttributes> configAttributes) {
SpringBootTest springBootTest = TestContextAnnotationUtils.findMergedAnnotation(testClass,
SpringBootTest.class);
return (springBootTest != null && restClientPresent) ? new RestTestClientContextCustomizer() : null;
}
}

View File

@ -4,6 +4,7 @@ org.springframework.boot.web.server.test.SpringBootTestRandomPortEnvironmentPost
# Spring Test Context Customizer Factories
org.springframework.test.context.ContextCustomizerFactory=\
org.springframework.boot.web.server.test.client.RestTestClientContextCustomizerFactory,\
org.springframework.boot.web.server.test.client.TestRestTemplateContextCustomizerFactory,\
org.springframework.boot.web.server.test.client.reactive.WebTestClientContextCustomizerFactory,\
org.springframework.boot.web.server.test.reactor.netty.DisableReactorResourceFactoryGlobalResourcesContextCustomizerFactory

View File

@ -0,0 +1,47 @@
/*
* 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 org.springframework.boot.web.server.test.client;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.test.web.servlet.client.RestTestClient;
import static org.assertj.core.api.Assertions.assertThat;
/**
* {@link ImportSelector} to check no {@link RestTestClient} definition is registered when
* config classes are processed.
*/
class NoRestTestClientBeanChecker implements ImportSelector, BeanFactoryAware {
@Override
public void setBeanFactory(BeanFactory beanFactory) {
assertThat(BeanFactoryUtils.beanNamesForTypeIncludingAncestors((ListableBeanFactory) beanFactory,
RestTestClient.class))
.isEmpty();
}
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[0];
}
}

View File

@ -0,0 +1,74 @@
/*
* 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 org.springframework.boot.web.server.test.client;
import org.junit.jupiter.api.Test;
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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.web.servlet.client.RestTestClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
/**
* Integration test for {@link RestTestClientContextCustomizer}.
*
* @author Stephane Nicoll
*/
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@DirtiesContext
class RestTestClientContextCustomizerIntegrationTests {
@Autowired
private RestTestClient webClient;
@Autowired
private RestTestClientBuilderCustomizer clientBuilderCustomizer;
@Test
void test() {
then(this.clientBuilderCustomizer).should().customize(any(RestTestClient.Builder.class));
this.webClient.get().uri("/").exchange().expectBody(String.class).isEqualTo("hello");
}
@Configuration(proxyBeanMethods = false)
@Import({ TestWebMvcConfiguration.class, NoRestTestClientBeanChecker.class })
@RestController
static class TestConfig {
@GetMapping("/")
String root() {
return "hello";
}
@Bean
RestTestClientBuilderCustomizer clientBuilderCustomizer() {
return mock(RestTestClientBuilderCustomizer.class);
}
}
}

View File

@ -0,0 +1,92 @@
/*
* 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 org.springframework.boot.web.server.test.client;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.web.server.test.client.RestTestClientContextCustomizer.RestTestClientRegistrar;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.test.context.MergedContextConfiguration;
import org.springframework.test.web.servlet.client.RestTestClient;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link RestTestClientContextCustomizer}.
*
* @author Moritz Halbritter
*/
class RestTestClientContextCustomizerTests {
@Test
void whenContextIsNotABeanDefinitionRegistryRestTestClientIsRegistered() {
new ApplicationContextRunner(TestApplicationContext::new)
.withInitializer(this::applyRestTestClientContextCustomizer)
.run((context) -> assertThat(context).hasSingleBean(RestTestClient.class));
}
@Test
void whenUsingAotGeneratedArtifactsRestTestClientIsNotRegistered() {
new ApplicationContextRunner().withSystemProperties("spring.aot.enabled:true")
.withInitializer(this::applyRestTestClientContextCustomizer)
.run((context) -> {
assertThat(context).doesNotHaveBean(RestTestClientRegistrar.class);
assertThat(context).doesNotHaveBean(RestTestClient.class);
});
}
@SuppressWarnings({ "unchecked", "rawtypes" })
void applyRestTestClientContextCustomizer(ConfigurableApplicationContext context) {
MergedContextConfiguration configuration = mock(MergedContextConfiguration.class);
given(configuration.getTestClass()).willReturn((Class) TestClass.class);
new RestTestClientContextCustomizer().customizeContext(context, configuration);
}
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
static class TestClass {
}
static class TestApplicationContext extends AbstractApplicationContext {
private final ConfigurableListableBeanFactory beanFactory = new DefaultListableBeanFactory();
@Override
protected void refreshBeanFactory() {
}
@Override
protected void closeBeanFactory() {
}
@Override
public ConfigurableListableBeanFactory getBeanFactory() {
return this.beanFactory;
}
}
}

View File

@ -0,0 +1,88 @@
/*
* 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 org.springframework.boot.web.server.test.client;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.tomcat.servlet.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.servlet.ServletWebServerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.web.servlet.client.RestTestClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.DispatcherServlet;
/**
* Integration test for {@link RestTestClientContextCustomizer} with a custom context
* path.
*
* @author Stephane Nicoll
*/
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@TestPropertySource(properties = "server.servlet.context-path=/test")
class RestTestClientContextCustomizerWithCustomContextPathIntegrationTests {
@Autowired
private RestTestClient webClient;
@Test
void test() {
this.webClient.get().uri("/").exchange().expectBody(String.class).isEqualTo("hello");
}
@Configuration(proxyBeanMethods = false)
@Import(NoRestTestClientBeanChecker.class)
@RestController
static class TestConfig {
@Value("${server.port:8080}")
private int port = 8080;
@Bean
DispatcherServlet dispatcherServlet() {
return new DispatcherServlet();
}
@Bean
ServletWebServerFactory webServerFactory() {
TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
factory.setPort(this.port);
factory.setContextPath("/test");
return factory;
}
@Bean
static PropertySourcesPlaceholderConfigurer propertyPlaceholder() {
return new PropertySourcesPlaceholderConfigurer();
}
@GetMapping("/")
String root() {
return "hello";
}
}
}

View File

@ -0,0 +1,71 @@
/*
* 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 org.springframework.boot.web.server.test.client;
import org.junit.jupiter.api.Test;
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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.test.web.servlet.client.RestTestClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Integration test for {@link RestTestClientContextCustomizer} with a custom client.
*
* @author Stephane Nicoll
*/
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class RestTestClientContextCustomizerWithOverridePathIntegrationTests {
@Autowired
private RestTestClient webClient;
@Test
void test() {
assertThat(this.webClient).isInstanceOf(CustomRestTestClient.class);
}
@Configuration(proxyBeanMethods = false)
@Import({ TestWebMvcConfiguration.class, NoRestTestClientBeanChecker.class })
@RestController
static class TestConfig {
@GetMapping("/")
String root() {
return "hello";
}
@Bean
CustomRestTestClient customRestTestClient() {
return mock(CustomRestTestClient.class);
}
}
interface CustomRestTestClient extends RestTestClient {
}
}

View File

@ -0,0 +1,52 @@
/*
* 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 org.springframework.boot.web.server.test.client;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.tomcat.servlet.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.servlet.ServletWebServerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
@Configuration(proxyBeanMethods = false)
@EnableWebMvc
class TestWebMvcConfiguration {
@Value("${server.port:8080}")
private int port = 8080;
@Bean
DispatcherServlet dispatcherServlet() {
return new DispatcherServlet();
}
@Bean
ServletWebServerFactory webServerFactory() {
TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
factory.setPort(this.port);
return factory;
}
@Bean
static PropertySourcesPlaceholderConfigurer propertyPlaceholder() {
return new PropertySourcesPlaceholderConfigurer();
}
}

View File

@ -25,6 +25,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplicat
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.server.autoconfigure.ServerProperties;
import org.springframework.boot.web.server.test.client.RestTestClientBuilderCustomizer;
import org.springframework.boot.web.server.test.client.reactive.WebTestClientBuilderCustomizer;
import org.springframework.boot.webmvc.autoconfigure.DispatcherServletPath;
import org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration;
@ -35,6 +36,8 @@ import org.springframework.context.annotation.Import;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.client.MockMvcWebTestClient;
import org.springframework.test.web.servlet.client.RestTestClient;
import org.springframework.web.client.RestClient;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.servlet.DispatcherServlet;
@ -82,4 +85,20 @@ public final class MockMvcAutoConfiguration {
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ RestClient.class, RestTestClient.class })
static class RestTestClientMockMvcConfiguration {
@Bean
@ConditionalOnMissingBean
RestTestClient restTestClient(MockMvc mockMvc, List<RestTestClientBuilderCustomizer> customizers) {
RestTestClient.Builder<?> builder = RestTestClient.bindTo(mockMvc);
for (RestTestClientBuilderCustomizer customizer : customizers) {
customizer.customize(builder);
}
return builder.build();
}
}
}

View File

@ -21,6 +21,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.boot.web.server.test.client.RestTestClientBuilderCustomizer;
import org.springframework.boot.web.server.test.client.reactive.WebTestClientBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@ -28,6 +29,8 @@ import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.RequestBuilder;
import org.springframework.test.web.servlet.assertj.MockMvcTester;
import org.springframework.test.web.servlet.client.RestTestClient;
import org.springframework.web.client.RestClient;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.servlet.DispatcherServlet;
@ -41,6 +44,7 @@ import static org.mockito.Mockito.mock;
*
* @author Madhura Bhave
* @author Brian Clozel
* @author Stephane Nicoll
*/
class MockMvcAutoConfigurationTests {
@ -99,6 +103,27 @@ class MockMvcAutoConfigurationTests {
});
}
@Test
void registersRestTestClient() {
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(RestTestClient.class));
}
@Test
void shouldNotRegisterRestTestClientIfRestClientIsMissing() {
this.contextRunner.withClassLoader(new FilteredClassLoader(RestClient.class))
.run((context) -> assertThat(context).doesNotHaveBean(RestTestClient.class));
}
@Test
void shouldApplyRestTestClientCustomizers() {
this.contextRunner.withUserConfiguration(RestTestClientCustomConfig.class).run((context) -> {
assertThat(context).hasSingleBean(RestTestClient.class);
assertThat(context).hasBean("myRestTestClientCustomizer");
then(context.getBean("myRestTestClientCustomizer", RestTestClientBuilderCustomizer.class)).should()
.customize(any(RestTestClient.Builder.class));
});
}
@Configuration(proxyBeanMethods = false)
static class WebTestClientCustomConfig {
@ -109,4 +134,14 @@ class MockMvcAutoConfigurationTests {
}
@Configuration(proxyBeanMethods = false)
static class RestTestClientCustomConfig {
@Bean
RestTestClientBuilderCustomizer myRestTestClientCustomizer() {
return mock(RestTestClientBuilderCustomizer.class);
}
}
}

View File

@ -29,6 +29,7 @@ import org.springframework.context.ApplicationContext;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.client.RestTestClient;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
@ -84,6 +85,11 @@ class MockMvcSpringBootTestIntegrationTests {
webTestClient.get().uri("/one").exchange().expectStatus().isOk().expectBody(String.class).isEqualTo("one");
}
@Test
void shouldTestWithRestTestClient(@Autowired RestTestClient restTestClient) {
restTestClient.get().uri("/one").exchange().expectStatus().isOk().expectBody(String.class).isEqualTo("one");
}
@Test
void shouldNotFailIfFormattingValueThrowsException(CapturedOutput output) throws Exception {
this.mvc.perform(get("/formatting")).andExpect(content().string("formatting")).andExpect(status().isOk());

View File

@ -29,6 +29,7 @@ import org.springframework.context.ApplicationContext;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.test.web.servlet.assertj.MockMvcTester;
import org.springframework.test.web.servlet.client.RestTestClient;
import static org.assertj.core.api.Assertions.assertThat;
@ -80,6 +81,11 @@ class MockMvcTesterSpringBootTestIntegrationTests {
webTestClient.get().uri("/one").exchange().expectStatus().isOk().expectBody(String.class).isEqualTo("one");
}
@Test
void shouldTestWithRestTestClient(@Autowired RestTestClient restTestClient) {
restTestClient.get().uri("/one").exchange().expectStatus().isOk().expectBody(String.class).isEqualTo("one");
}
@Test
void shouldNotFailIfFormattingValueThrowsException(CapturedOutput output) {
assertThat(this.mvc.get().uri("/formatting")).hasStatusOk().hasBodyTextEqualTo("formatting");