Skip to content

Commit 7878407

Browse files
committed
Add support for RestTestClient
This commit adds support for RestTestClient for MockMvc and integration tests. Closes gh-47335
1 parent e2ba4da commit 7878407

File tree

19 files changed

+854
-7
lines changed

19 files changed

+854
-7
lines changed

documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/spring-boot-applications.adoc

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -141,10 +141,11 @@ include-code::MyApplicationArgumentTests[]
141141
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.
142142

143143
With Spring MVC, we can query our web endpoints using {url-spring-framework-docs}/testing/mockmvc.html[`MockMvc`].
144-
Three integrations are available:
144+
The following integrations are available:
145145

146146
* The regular {url-spring-framework-docs}/testing/mockmvc/hamcrest.html[`MockMvc`] that uses Hamcrest.
147147
* {url-spring-framework-docs}/testing/mockmvc/assertj.html[`MockMvcTester`] that wraps javadoc:org.springframework.test.web.servlet.MockMvc[] and uses AssertJ.
148+
* {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.
148149
* {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.
149150

150151
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
176177
If you use `@SpringBootTest(webEnvironment=WebEnvironment.RANDOM_PORT)`, an available port is picked at random each time your test runs.
177178

178179
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.
179-
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:
180+
181+
For convenience, tests that need to make REST calls to the started server can additionally autowire a
182+
{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:
183+
184+
include-code::MyRandomPortRestTestClientTests[]
185+
186+
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:
180187

181188
include-code::MyRandomPortWebTestClientTests[]
182189

183190
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`.
184191

185-
This setup requires `spring-webflux` on the classpath.
186-
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:
192+
The `spring-boot-web-server-test` modules also provides a javadoc:org.springframework.boot.web.server.test.client.TestRestTemplate[] facility:
187193

188194
include-code::MyRandomPortTestRestTemplateTests[]
189195

190196

191197

198+
[[testing.spring-boot-applications.customizing-rest-test-client]]
199+
== Customizing RestTestClient
200+
201+
To customize the javadoc:org.springframework.test.web.servlet.client.RestTestClient[] bean, configure a javadoc:org.springframework.boot.web.server.test.client.RestTestClientBuilderCustomizer[] bean.
202+
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[].
203+
204+
205+
192206
[[testing.spring-boot-applications.customizing-web-test-client]]
193207
== Customizing WebTestClient
194208

documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/test-utilities.adoc

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,9 @@ In either case, the template is fault tolerant.
4848
This means that it behaves in a test-friendly way by not throwing exceptions on 4xx and 5xx errors.
4949
Instead, such errors can be detected through the returned javadoc:org.springframework.http.ResponseEntity[] and its status code.
5050

51-
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].
52-
It provides a fluent API for assertions, unlike javadoc:org.springframework.boot.test.web.client.TestRestTemplate[].
51+
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].
52+
53+
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].
5354

5455
It is recommended, but not mandatory, to use the Apache HTTP Client (version 5.1 or better).
5556
If you have that on your classpath, the javadoc:org.springframework.boot.test.web.client.TestRestTemplate[] responds by configuring the client appropriately.

documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/testing/springbootapplications/withmockenvironment/MyMockMvcTests.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.springframework.test.web.reactive.server.WebTestClient;
2525
import org.springframework.test.web.servlet.MockMvc;
2626
import org.springframework.test.web.servlet.assertj.MockMvcTester;
27+
import org.springframework.test.web.servlet.client.RestTestClient;
2728

2829
import static org.assertj.core.api.Assertions.assertThat;
2930
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
@@ -45,6 +46,17 @@ void testWithMockMvcTester(@Autowired MockMvcTester mvc) {
4546
assertThat(mvc.get().uri("/")).hasStatusOk().hasBodyTextEqualTo("Hello World");
4647
}
4748

49+
@Test
50+
void testWithRestTestClient(@Autowired RestTestClient webClient) {
51+
// @formatter:off
52+
webClient
53+
.get().uri("/")
54+
.exchange()
55+
.expectStatus().isOk()
56+
.expectBody(String.class).isEqualTo("Hello World");
57+
// @formatter:on
58+
}
59+
4860
// If Spring WebFlux is on the classpath, you can drive MVC tests with a WebTestClient
4961
@Test
5062
void testWithWebTestClient(@Autowired WebTestClient webClient) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright 2012-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.docs.testing.springbootapplications.withrunningserver;
18+
19+
import org.junit.jupiter.api.Test;
20+
21+
import org.springframework.beans.factory.annotation.Autowired;
22+
import org.springframework.boot.test.context.SpringBootTest;
23+
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
24+
import org.springframework.test.web.servlet.client.RestTestClient;
25+
26+
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
27+
class MyRandomPortRestTestClientTests {
28+
29+
@Test
30+
void exampleTest(@Autowired RestTestClient webClient) {
31+
// @formatter:off
32+
webClient
33+
.get().uri("/")
34+
.exchange()
35+
.expectStatus().isOk()
36+
.expectBody(String.class).isEqualTo("Hello World");
37+
// @formatter:on
38+
}
39+
40+
}

module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/RootUriBuilderFactory.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,12 @@
3131
*/
3232
public class RootUriBuilderFactory extends RootUriTemplateHandler implements UriBuilderFactory {
3333

34-
RootUriBuilderFactory(String rootUri, UriTemplateHandler delegate) {
34+
/**
35+
* Create an instance with the root URI to use.
36+
* @param rootUri the root URI
37+
* @param delegate the {@link UriTemplateHandler} to delegate to
38+
*/
39+
public RootUriBuilderFactory(String rootUri, UriTemplateHandler delegate) {
3540
super(rootUri, delegate);
3641
}
3742

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright 2012-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.web.server.test.client;
18+
19+
import org.springframework.test.web.servlet.client.RestTestClient;
20+
21+
/**
22+
* A customizer that can be implemented by beans wishing to customize the
23+
* {@link RestTestClient.Builder} to fine-tine its auto-configuration before a
24+
* {@link RestTestClient} is created.
25+
*
26+
* @author Stephane Nicoll
27+
* @since 4.0.0
28+
*/
29+
@FunctionalInterface
30+
public interface RestTestClientBuilderCustomizer {
31+
32+
/**
33+
* Customize the given {@link RestTestClient.Builder Builder}.
34+
* @param builder the builder
35+
*/
36+
void customize(RestTestClient.Builder<?> builder);
37+
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
/*
2+
* Copyright 2012-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.web.server.test.client;
18+
19+
import org.jspecify.annotations.Nullable;
20+
21+
import org.springframework.aot.AotDetector;
22+
import org.springframework.beans.BeansException;
23+
import org.springframework.beans.factory.BeanFactory;
24+
import org.springframework.beans.factory.BeanFactoryAware;
25+
import org.springframework.beans.factory.BeanFactoryUtils;
26+
import org.springframework.beans.factory.FactoryBean;
27+
import org.springframework.beans.factory.ListableBeanFactory;
28+
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
29+
import org.springframework.beans.factory.config.BeanDefinition;
30+
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
31+
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
32+
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
33+
import org.springframework.beans.factory.support.RootBeanDefinition;
34+
import org.springframework.boot.restclient.RootUriBuilderFactory;
35+
import org.springframework.boot.test.context.SpringBootTest;
36+
import org.springframework.boot.web.server.reactive.AbstractReactiveWebServerFactory;
37+
import org.springframework.context.ApplicationContext;
38+
import org.springframework.context.ApplicationContextAware;
39+
import org.springframework.context.ConfigurableApplicationContext;
40+
import org.springframework.context.annotation.ConfigurationClassPostProcessor;
41+
import org.springframework.core.Ordered;
42+
import org.springframework.test.context.ContextCustomizer;
43+
import org.springframework.test.context.MergedContextConfiguration;
44+
import org.springframework.test.context.TestContextAnnotationUtils;
45+
import org.springframework.test.web.servlet.client.RestTestClient;
46+
import org.springframework.util.Assert;
47+
48+
/**
49+
* {@link ContextCustomizer} for {@link RestTestClient}.
50+
*
51+
* @author Stephane Nicoll
52+
*/
53+
class RestTestClientContextCustomizer implements ContextCustomizer {
54+
55+
@Override
56+
public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) {
57+
if (AotDetector.useGeneratedArtifacts()) {
58+
return;
59+
}
60+
SpringBootTest springBootTest = TestContextAnnotationUtils.findMergedAnnotation(mergedConfig.getTestClass(),
61+
SpringBootTest.class);
62+
Assert.state(springBootTest != null, "'springBootTest' must not be null");
63+
if (springBootTest.webEnvironment().isEmbedded()) {
64+
registerRestTestClient(context);
65+
}
66+
}
67+
68+
private void registerRestTestClient(ConfigurableApplicationContext context) {
69+
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
70+
if (beanFactory instanceof BeanDefinitionRegistry registry) {
71+
registerRestTestClient(registry);
72+
}
73+
}
74+
75+
private void registerRestTestClient(BeanDefinitionRegistry registry) {
76+
RootBeanDefinition definition = new RootBeanDefinition(RestTestClientRegistrar.class);
77+
definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
78+
registry.registerBeanDefinition(RestTestClientRegistrar.class.getName(), definition);
79+
}
80+
81+
@Override
82+
public boolean equals(@Nullable Object obj) {
83+
return (obj != null) && (obj.getClass() == getClass());
84+
}
85+
86+
@Override
87+
public int hashCode() {
88+
return getClass().hashCode();
89+
}
90+
91+
/**
92+
* {@link BeanDefinitionRegistryPostProcessor} that runs after the
93+
* {@link ConfigurationClassPostProcessor} and add a {@link RestTestClientFactory}
94+
* bean definition when a {@link RestTestClient} hasn't already been registered.
95+
*/
96+
static class RestTestClientRegistrar implements BeanDefinitionRegistryPostProcessor, Ordered, BeanFactoryAware {
97+
98+
@SuppressWarnings("NullAway.Init")
99+
private BeanFactory beanFactory;
100+
101+
@Override
102+
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
103+
this.beanFactory = beanFactory;
104+
}
105+
106+
@Override
107+
public int getOrder() {
108+
return Ordered.LOWEST_PRECEDENCE;
109+
}
110+
111+
@Override
112+
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
113+
if (AotDetector.useGeneratedArtifacts()) {
114+
return;
115+
}
116+
if (BeanFactoryUtils.beanNamesForTypeIncludingAncestors((ListableBeanFactory) this.beanFactory,
117+
RestTestClient.class, false, false).length == 0) {
118+
registry.registerBeanDefinition(RestTestClient.class.getName(),
119+
new RootBeanDefinition(RestTestClientFactory.class));
120+
}
121+
122+
}
123+
124+
@Override
125+
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
126+
}
127+
128+
}
129+
130+
/**
131+
* {@link FactoryBean} used to create and configure a {@link RestTestClient}.
132+
*/
133+
public static class RestTestClientFactory implements FactoryBean<RestTestClient>, ApplicationContextAware {
134+
135+
@SuppressWarnings("NullAway.Init")
136+
private ApplicationContext applicationContext;
137+
138+
private @Nullable RestTestClient object;
139+
140+
@Override
141+
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
142+
this.applicationContext = applicationContext;
143+
}
144+
145+
@Override
146+
public boolean isSingleton() {
147+
return true;
148+
}
149+
150+
@Override
151+
public Class<?> getObjectType() {
152+
return RestTestClient.class;
153+
}
154+
155+
@Override
156+
public RestTestClient getObject() {
157+
if (this.object == null) {
158+
this.object = createRestTestClient();
159+
}
160+
return this.object;
161+
}
162+
163+
private RestTestClient createRestTestClient() {
164+
boolean sslEnabled = isSslEnabled(this.applicationContext);
165+
LocalHostUriTemplateHandler handler = new LocalHostUriTemplateHandler(
166+
this.applicationContext.getEnvironment(), sslEnabled ? "https" : "http");
167+
RestTestClient.Builder<?> builder = RestTestClient.bindToServer();
168+
customizeRestTestClientBuilder(builder, this.applicationContext);
169+
return builder.uriBuilderFactory(new RootUriBuilderFactory(handler.getRootUri(), handler)).build();
170+
}
171+
172+
private boolean isSslEnabled(ApplicationContext context) {
173+
try {
174+
AbstractReactiveWebServerFactory webServerFactory = context
175+
.getBean(AbstractReactiveWebServerFactory.class);
176+
return webServerFactory.getSsl() != null && webServerFactory.getSsl().isEnabled();
177+
}
178+
catch (NoSuchBeanDefinitionException ex) {
179+
return false;
180+
}
181+
}
182+
183+
private void customizeRestTestClientBuilder(RestTestClient.Builder<?> clientBuilder,
184+
ApplicationContext context) {
185+
for (RestTestClientBuilderCustomizer customizer : context
186+
.getBeansOfType(RestTestClientBuilderCustomizer.class)
187+
.values()) {
188+
customizer.customize(clientBuilder);
189+
}
190+
}
191+
192+
}
193+
194+
}

0 commit comments

Comments
 (0)