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. 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`]. 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. * 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/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. * {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: 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. 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. 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[] 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`. 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. The `spring-boot-web-server-test` modules also provides a javadoc:org.springframework.boot.web.server.test.client.TestRestTemplate[] facility:
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:
include-code::MyRandomPortTestRestTemplateTests[] 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]] [[testing.spring-boot-applications.customizing-web-test-client]]
== Customizing WebTestClient == 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. 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. 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]. 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].
It provides a fluent API for assertions, unlike javadoc:org.springframework.boot.test.web.client.TestRestTemplate[].
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). 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. 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.reactive.server.WebTestClient;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.assertj.MockMvcTester; 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.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
@ -45,6 +46,17 @@ class MyMockMvcTests {
assertThat(mvc.get().uri("/")).hasStatusOk().hasBodyTextEqualTo("Hello World"); 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 // If Spring WebFlux is on the classpath, you can drive MVC tests with a WebTestClient
@Test @Test
void testWithWebTestClient(@Autowired WebTestClient webClient) { 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 { 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); 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 # Spring Test Context Customizer Factories
org.springframework.test.context.ContextCustomizerFactory=\ 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.TestRestTemplateContextCustomizerFactory,\
org.springframework.boot.web.server.test.client.reactive.WebTestClientContextCustomizerFactory,\ org.springframework.boot.web.server.test.client.reactive.WebTestClientContextCustomizerFactory,\
org.springframework.boot.web.server.test.reactor.netty.DisableReactorResourceFactoryGlobalResourcesContextCustomizerFactory 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.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.server.autoconfigure.ServerProperties; 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.web.server.test.client.reactive.WebTestClientBuilderCustomizer;
import org.springframework.boot.webmvc.autoconfigure.DispatcherServletPath; import org.springframework.boot.webmvc.autoconfigure.DispatcherServletPath;
import org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration; 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.reactive.server.WebTestClient;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.client.MockMvcWebTestClient; 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.reactive.function.client.WebClient;
import org.springframework.web.servlet.DispatcherServlet; 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.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner; 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.boot.web.server.test.client.reactive.WebTestClientBuilderCustomizer;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; 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.MockMvc;
import org.springframework.test.web.servlet.RequestBuilder; import org.springframework.test.web.servlet.RequestBuilder;
import org.springframework.test.web.servlet.assertj.MockMvcTester; 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.reactive.function.client.WebClient;
import org.springframework.web.servlet.DispatcherServlet; import org.springframework.web.servlet.DispatcherServlet;
@ -41,6 +44,7 @@ import static org.mockito.Mockito.mock;
* *
* @author Madhura Bhave * @author Madhura Bhave
* @author Brian Clozel * @author Brian Clozel
* @author Stephane Nicoll
*/ */
class MockMvcAutoConfigurationTests { 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) @Configuration(proxyBeanMethods = false)
static class WebTestClientCustomConfig { 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.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.test.web.servlet.MockMvc; 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.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 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"); 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 @Test
void shouldNotFailIfFormattingValueThrowsException(CapturedOutput output) throws Exception { void shouldNotFailIfFormattingValueThrowsException(CapturedOutput output) throws Exception {
this.mvc.perform(get("/formatting")).andExpect(content().string("formatting")).andExpect(status().isOk()); 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.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.test.web.servlet.assertj.MockMvcTester; 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.assertj.core.api.Assertions.assertThat;
@ -80,6 +81,11 @@ class MockMvcTesterSpringBootTestIntegrationTests {
webTestClient.get().uri("/one").exchange().expectStatus().isOk().expectBody(String.class).isEqualTo("one"); 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 @Test
void shouldNotFailIfFormattingValueThrowsException(CapturedOutput output) { void shouldNotFailIfFormattingValueThrowsException(CapturedOutput output) {
assertThat(this.mvc.get().uri("/formatting")).hasStatusOk().hasBodyTextEqualTo("formatting"); assertThat(this.mvc.get().uri("/formatting")).hasStatusOk().hasBodyTextEqualTo("formatting");