Skip to content

Commit ff99de4

Browse files
Configure a RestClient.Builder with RestClientTest
This commit adds support for configuring a `RestClient.Builder` and `MockRestServiceServer` support for the `RestClient` when using `@RestClientTest` sliced tests. Closes gh-37033
1 parent d725914 commit ff99de4

File tree

26 files changed

+868
-57
lines changed

26 files changed

+868
-57
lines changed

spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc

+7-3
Original file line numberDiff line numberDiff line change
@@ -735,17 +735,21 @@ include::code:server/MyDataLdapTests[]
735735
[[features.testing.spring-boot-applications.autoconfigured-rest-client]]
736736
==== Auto-configured REST Clients
737737
You can use the `@RestClientTest` annotation to test REST clients.
738-
By default, it auto-configures Jackson, GSON, and Jsonb support, configures a `RestTemplateBuilder`, and adds support for `MockRestServiceServer`.
738+
By default, it auto-configures Jackson, GSON, and Jsonb support, configures a `RestTemplateBuilder` and a `RestClient.Builder`, and adds support for `MockRestServiceServer`.
739739
Regular `@Component` and `@ConfigurationProperties` beans are not scanned when the `@RestClientTest` annotation is used.
740740
`@EnableConfigurationProperties` can be used to include `@ConfigurationProperties` beans.
741741

742742
TIP: A list of the auto-configuration settings that are enabled by `@RestClientTest` can be <<test-auto-configuration#appendix.test-auto-configuration,found in the appendix>>.
743743

744-
The specific beans that you want to test should be specified by using the `value` or `components` attribute of `@RestClientTest`, as shown in the following example:
744+
The specific beans that you want to test should be specified by using the `value` or `components` attribute of `@RestClientTest`.
745745

746-
include::code:MyRestClientTests[]
746+
When using a `RestTemplateBuilder` in the beans under test and `RestTemplateBuilder.rootUri(String rootUri)` has been called when building the `RestTemplate`, then the root URI should be omitted from the `MockRestServiceServer` expectations as shown in the following example:
747747

748+
include::code:MyRestTemplateServiceTests[]
748749

750+
When using a `RestClient.Builder` in the beans under test, or when using a `RestTemplateBuilder` without calling `rootUri(String rootURI)`, the full URI must be used in the `MockRestServiceServer` expectations as shown in the following example:
751+
752+
include::code:MyRestClientServiceTests[]
749753

750754
[[features.testing.spring-boot-applications.autoconfigured-spring-restdocs]]
751755
==== Auto-configured Spring REST Docs Tests
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright 2012-2023 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.features.testing.springbootapplications.autoconfiguredrestclient;
18+
19+
import org.junit.jupiter.api.Test;
20+
21+
import org.springframework.beans.factory.annotation.Autowired;
22+
import org.springframework.boot.test.autoconfigure.web.client.RestClientTest;
23+
import org.springframework.http.MediaType;
24+
import org.springframework.test.web.client.MockRestServiceServer;
25+
26+
import static org.assertj.core.api.Assertions.assertThat;
27+
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
28+
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
29+
30+
@RestClientTest(RemoteVehicleDetailsService.class)
31+
class MyRestClientServiceTests {
32+
33+
@Autowired
34+
private RemoteVehicleDetailsService service;
35+
36+
@Autowired
37+
private MockRestServiceServer server;
38+
39+
@Test
40+
void getVehicleDetailsWhenResultIsSuccessShouldReturnDetails() {
41+
this.server.expect(requestTo("https://example.com/greet/details"))
42+
.andRespond(withSuccess("hello", MediaType.TEXT_PLAIN));
43+
String greeting = this.service.callRestService();
44+
assertThat(greeting).isEqualTo("hello");
45+
}
46+
47+
}
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2022 the original author or authors.
2+
* Copyright 2012-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -28,7 +28,7 @@
2828
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
2929

3030
@RestClientTest(RemoteVehicleDetailsService.class)
31-
class MyRestClientTests {
31+
class MyRestTemplateServiceTests {
3232

3333
@Autowired
3434
private RemoteVehicleDetailsService service;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright 2012-2023 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.features.testing.springbootapplications.autoconfiguredrestclient
18+
19+
import org.assertj.core.api.Assertions.assertThat
20+
import org.junit.jupiter.api.Test
21+
import org.springframework.beans.factory.annotation.Autowired
22+
import org.springframework.boot.test.autoconfigure.web.client.RestClientTest
23+
import org.springframework.http.MediaType
24+
import org.springframework.test.web.client.MockRestServiceServer
25+
import org.springframework.test.web.client.match.MockRestRequestMatchers
26+
import org.springframework.test.web.client.response.MockRestResponseCreators
27+
28+
@RestClientTest(RemoteVehicleDetailsService::class)
29+
class MyRestClientServiceTests(
30+
@Autowired val service: RemoteVehicleDetailsService,
31+
@Autowired val server: MockRestServiceServer) {
32+
33+
@Test
34+
fun getVehicleDetailsWhenResultIsSuccessShouldReturnDetails(): Unit {
35+
server.expect(MockRestRequestMatchers.requestTo("https://example.com/greet/details"))
36+
.andRespond(MockRestResponseCreators.withSuccess("hello", MediaType.TEXT_PLAIN))
37+
val greeting = service.callRestService()
38+
assertThat(greeting).isEqualTo("hello")
39+
}
40+
41+
}
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2022 the original author or authors.
2+
* Copyright 2012-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -26,7 +26,7 @@ import org.springframework.test.web.client.match.MockRestRequestMatchers
2626
import org.springframework.test.web.client.response.MockRestResponseCreators
2727

2828
@RestClientTest(RemoteVehicleDetailsService::class)
29-
class MyRestClientTests(
29+
class MyRestTemplateServiceTests(
3030
@Autowired val service: RemoteVehicleDetailsService,
3131
@Autowired val server: MockRestServiceServer) {
3232

spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/client/AutoConfigureMockRestServiceServer.java

+12-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2019 the original author or authors.
2+
* Copyright 2012-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -25,20 +25,26 @@
2525

2626
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
2727
import org.springframework.boot.test.autoconfigure.properties.PropertyMapping;
28+
import org.springframework.boot.test.web.client.MockServerRestClientCustomizer;
2829
import org.springframework.boot.test.web.client.MockServerRestTemplateCustomizer;
2930
import org.springframework.boot.web.client.RestTemplateBuilder;
3031
import org.springframework.test.web.client.MockRestServiceServer;
32+
import org.springframework.web.client.RestClient;
3133

3234
/**
3335
* Annotation that can be applied to a test class to enable and configure
3436
* auto-configuration of a single {@link MockRestServiceServer}. Only useful when a single
35-
* call is made to {@link RestTemplateBuilder}. If multiple
36-
* {@link org.springframework.web.client.RestTemplate RestTemplates} are in use, inject
37+
* call is made to {@link RestTemplateBuilder} or {@link RestClient.Builder}. If multiple
38+
* {@link org.springframework.web.client.RestTemplate RestTemplates} or
39+
* {@link org.springframework.web.client.RestClient RestClients} are in use, inject a
3740
* {@link MockServerRestTemplateCustomizer} and use
3841
* {@link MockServerRestTemplateCustomizer#getServer(org.springframework.web.client.RestTemplate)
39-
* getServer(RestTemplate)} or bind a {@link MockRestServiceServer} directly.
42+
* getServer(RestTemplate)}, or inject a {@link MockServerRestClientCustomizer} and use
43+
* {@link MockServerRestClientCustomizer#getServer(org.springframework.web.client.RestClient.Builder)
44+
* * getServer(RestClient.Builder)}, or bind a {@link MockRestServiceServer} directly.
4045
*
4146
* @author Phillip Webb
47+
* @author Scott Frederick
4248
* @since 1.4.0
4349
* @see MockServerRestTemplateCustomizer
4450
*/
@@ -51,7 +57,8 @@
5157
public @interface AutoConfigureMockRestServiceServer {
5258

5359
/**
54-
* If {@link MockServerRestTemplateCustomizer} should be enabled and
60+
* If {@link MockServerRestTemplateCustomizer} and
61+
* {@link MockServerRestClientCustomizer} should be enabled and
5562
* {@link MockRestServiceServer} beans should be registered. Defaults to {@code true}
5663
* @return if mock support is enabled
5764
*/

spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/client/MockRestServiceServerAutoConfiguration.java

+47-16
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@
1919
import java.io.IOException;
2020
import java.lang.reflect.Constructor;
2121
import java.time.Duration;
22+
import java.util.Collection;
2223
import java.util.Map;
2324

2425
import org.springframework.boot.autoconfigure.AutoConfiguration;
2526
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
27+
import org.springframework.boot.test.web.client.MockServerRestClientCustomizer;
2628
import org.springframework.boot.test.web.client.MockServerRestTemplateCustomizer;
2729
import org.springframework.context.annotation.Bean;
2830
import org.springframework.http.client.ClientHttpRequest;
@@ -33,12 +35,14 @@
3335
import org.springframework.test.web.client.RequestMatcher;
3436
import org.springframework.test.web.client.ResponseActions;
3537
import org.springframework.util.Assert;
38+
import org.springframework.web.client.RestClient;
3639
import org.springframework.web.client.RestTemplate;
3740

3841
/**
3942
* Auto-configuration for {@link MockRestServiceServer} support.
4043
*
4144
* @author Phillip Webb
45+
* @author Scott Frederick
4246
* @since 1.4.0
4347
* @see AutoConfigureMockRestServiceServer
4448
*/
@@ -52,21 +56,29 @@ public MockServerRestTemplateCustomizer mockServerRestTemplateCustomizer() {
5256
}
5357

5458
@Bean
55-
public MockRestServiceServer mockRestServiceServer(MockServerRestTemplateCustomizer customizer) {
59+
public MockServerRestClientCustomizer mockServerRestClientCustomizer() {
60+
return new MockServerRestClientCustomizer();
61+
}
62+
63+
@Bean
64+
public MockRestServiceServer mockRestServiceServer(MockServerRestTemplateCustomizer restTemplateCustomizer,
65+
MockServerRestClientCustomizer restClientCustomizer) {
5666
try {
57-
return createDeferredMockRestServiceServer(customizer);
67+
return createDeferredMockRestServiceServer(restTemplateCustomizer, restClientCustomizer);
5868
}
5969
catch (Exception ex) {
6070
throw new IllegalStateException(ex);
6171
}
6272
}
6373

64-
private MockRestServiceServer createDeferredMockRestServiceServer(MockServerRestTemplateCustomizer customizer)
65-
throws Exception {
74+
private MockRestServiceServer createDeferredMockRestServiceServer(
75+
MockServerRestTemplateCustomizer restTemplateCustomizer,
76+
MockServerRestClientCustomizer restClientCustomizer) throws Exception {
6677
Constructor<MockRestServiceServer> constructor = MockRestServiceServer.class
6778
.getDeclaredConstructor(RequestExpectationManager.class);
6879
constructor.setAccessible(true);
69-
return constructor.newInstance(new DeferredRequestExpectationManager(customizer));
80+
return constructor
81+
.newInstance(new DeferredRequestExpectationManager(restTemplateCustomizer, restClientCustomizer));
7082
}
7183

7284
/**
@@ -77,10 +89,14 @@ private MockRestServiceServer createDeferredMockRestServiceServer(MockServerRest
7789
*/
7890
private static class DeferredRequestExpectationManager implements RequestExpectationManager {
7991

80-
private final MockServerRestTemplateCustomizer customizer;
92+
private final MockServerRestTemplateCustomizer restTemplateCustomizer;
93+
94+
private final MockServerRestClientCustomizer restClientCustomizer;
8195

82-
DeferredRequestExpectationManager(MockServerRestTemplateCustomizer customizer) {
83-
this.customizer = customizer;
96+
DeferredRequestExpectationManager(MockServerRestTemplateCustomizer restTemplateCustomizer,
97+
MockServerRestClientCustomizer restClientCustomizer) {
98+
this.restTemplateCustomizer = restTemplateCustomizer;
99+
this.restClientCustomizer = restClientCustomizer;
84100
}
85101

86102
@Override
@@ -105,19 +121,34 @@ public void verify(Duration timeout) {
105121

106122
@Override
107123
public void reset() {
108-
Map<RestTemplate, RequestExpectationManager> expectationManagers = this.customizer.getExpectationManagers();
124+
resetExpectations(this.restTemplateCustomizer.getExpectationManagers().values());
125+
resetExpectations(this.restClientCustomizer.getExpectationManagers().values());
126+
}
127+
128+
private void resetExpectations(Collection<RequestExpectationManager> expectationManagers) {
109129
if (expectationManagers.size() == 1) {
110-
getDelegate().reset();
130+
expectationManagers.iterator().next().reset();
111131
}
112132
}
113133

114134
private RequestExpectationManager getDelegate() {
115-
Map<RestTemplate, RequestExpectationManager> expectationManagers = this.customizer.getExpectationManagers();
116-
Assert.state(!expectationManagers.isEmpty(), "Unable to use auto-configured MockRestServiceServer since "
117-
+ "MockServerRestTemplateCustomizer has not been bound to a RestTemplate");
118-
Assert.state(expectationManagers.size() == 1, "Unable to use auto-configured MockRestServiceServer since "
119-
+ "MockServerRestTemplateCustomizer has been bound to more than one RestTemplate");
120-
return expectationManagers.values().iterator().next();
135+
Map<RestTemplate, RequestExpectationManager> restTemplateExpectationManagers = this.restTemplateCustomizer
136+
.getExpectationManagers();
137+
Map<RestClient.Builder, RequestExpectationManager> restClientExpectationManagers = this.restClientCustomizer
138+
.getExpectationManagers();
139+
Assert.state(!(restTemplateExpectationManagers.isEmpty() && restClientExpectationManagers.isEmpty()),
140+
"Unable to use auto-configured MockRestServiceServer since "
141+
+ "a mock server customizer has not been bound to a RestTemplate or RestClient");
142+
if (!restTemplateExpectationManagers.isEmpty()) {
143+
Assert.state(restTemplateExpectationManagers.size() == 1,
144+
"Unable to use auto-configured MockRestServiceServer since "
145+
+ "MockServerRestTemplateCustomizer has been bound to more than one RestTemplate");
146+
return restTemplateExpectationManagers.values().iterator().next();
147+
}
148+
Assert.state(restClientExpectationManagers.size() == 1,
149+
"Unable to use auto-configured MockRestServiceServer since "
150+
+ "MockServerRestClientCustomizer has been bound to more than one RestClient");
151+
return restClientExpectationManagers.values().iterator().next();
121152
}
122153

123154
}

spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/client/RestClientTest.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2019 the original author or authors.
2+
* Copyright 2012-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -38,11 +38,12 @@
3838
import org.springframework.test.context.BootstrapWith;
3939
import org.springframework.test.context.junit.jupiter.SpringExtension;
4040
import org.springframework.test.web.client.MockRestServiceServer;
41+
import org.springframework.web.client.RestClient;
4142
import org.springframework.web.client.RestTemplate;
4243

4344
/**
4445
* Annotation for a Spring rest client test that focuses <strong>only</strong> on beans
45-
* that use {@link RestTemplateBuilder}.
46+
* that use {@link RestTemplateBuilder} or {@link RestClient.Builder}.
4647
* <p>
4748
* Using this annotation will disable full auto-configuration and instead apply only
4849
* configuration relevant to rest client tests (i.e. Jackson or GSON auto-configuration

spring-boot-project/spring-boot-test-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.test.autoconfigure.web.client.AutoConfigureWebClient.imports

+1
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration
66
org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration
77
org.springframework.boot.autoconfigure.jsonb.JsonbAutoConfiguration
88
org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration
9+
org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration
910
org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright 2012-2023 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.test.autoconfigure.web.client;
18+
19+
import org.springframework.stereotype.Service;
20+
import org.springframework.web.client.RestClient;
21+
import org.springframework.web.client.RestClient.Builder;
22+
23+
/**
24+
* A second example web client used with {@link RestClientTest @RestClientTest} tests.
25+
*
26+
* @author Scott Frederick
27+
*/
28+
@Service
29+
public class AnotherExampleRestClientService {
30+
31+
private final Builder builder;
32+
33+
private final RestClient restClient;
34+
35+
public AnotherExampleRestClientService(RestClient.Builder builder) {
36+
this.builder = builder;
37+
this.restClient = builder.baseUrl("https://example.com").build();
38+
}
39+
40+
protected Builder getRestClientBuilder() {
41+
return this.builder;
42+
}
43+
44+
public String test() {
45+
return this.restClient.get().uri("/test").retrieve().toEntity(String.class).getBody();
46+
}
47+
48+
}

0 commit comments

Comments
 (0)