Skip to content

Commit 4bcc243

Browse files
simonbaslerstoyanchev
authored andcommitted
Add ExecutingResponseCreator
This commit adds a new `ResponseCreator` implementation that uses a `ClientHttpRequestFactory` to perform an actual request. Closes gh-29721
1 parent ae7cff3 commit 4bcc243

File tree

5 files changed

+300
-0
lines changed

5 files changed

+300
-0
lines changed

framework-docs/src/docs/asciidoc/testing/spring-mvc-test-client.adoc

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,56 @@ logic but without running a server. The following example shows how to do so:
117117
// Test code that uses the above RestTemplate ...
118118
----
119119

120+
In the more specific cases where total isolation isn't desired and some integration testing
121+
of one or more calls is needed, a specific `ResponseCreator` can be set up in advance and
122+
used to perform actual requests and assert the response.
123+
The following example shows how to set up and use the `ExecutingResponseCreator` to do so:
124+
125+
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
126+
.Java
127+
----
128+
RestTemplate restTemplate = new RestTemplate();
129+
130+
// Make sure to capture the request factory of the RestTemplate before binding
131+
ExecutingResponseCreator withActualResponse = new ExecutingResponseCreator(restTemplate.getRequestFactory());
132+
133+
MockRestServiceServer mockServer = MockRestServiceServer.bindTo(restTemplate).build();
134+
mockServer.expect(requestTo("/greeting")).andRespond(withActualResponse);
135+
136+
// Test code that uses the above RestTemplate ...
137+
138+
mockServer.verify();
139+
----
140+
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
141+
.Kotlin
142+
----
143+
val restTemplate = RestTemplate()
144+
145+
// Make sure to capture the request factory of the RestTemplate before binding
146+
val withActualResponse = new ExecutingResponseCreator(restTemplate.getRequestFactory())
147+
148+
val mockServer = MockRestServiceServer.bindTo(restTemplate).build()
149+
mockServer.expect(requestTo("/profile")).andRespond(withSuccess())
150+
mockServer.expect(requestTo("/quoteOfTheDay")).andRespond(withActualResponse)
151+
152+
// Test code that uses the above RestTemplate ...
153+
154+
mockServer.verify()
155+
----
156+
157+
In the preceding example, we create the `ExecutingResponseCreator` using the
158+
`ClientHttpRequestFactory` from the `RestTemplate` _before_ `MockRestServiceServer` replaces
159+
it with the custom one.
160+
Then we define expectations with two kinds of response:
161+
162+
* a stub `200` response for the `/profile` endpoint (no actual request will be executed)
163+
* an "executing response" for the `/quoteOfTheDay` endpoint
164+
165+
In the second case, the request is executed by the `ClientHttpRequestFactory` that was
166+
captured earlier. This generates a response that could e.g. come from an actual remote server,
167+
depending on how the `RestTemplate` was originally configured, and MockMVC can be further
168+
used to assert the content of the response.
169+
120170
[[spring-mvc-test-client-static-imports]]
121171
== Static Imports
122172

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright 2002-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.test.web.client.response;
18+
19+
import java.io.IOException;
20+
21+
import org.springframework.http.client.ClientHttpRequest;
22+
import org.springframework.http.client.ClientHttpRequestFactory;
23+
import org.springframework.http.client.ClientHttpResponse;
24+
import org.springframework.mock.http.client.MockClientHttpRequest;
25+
import org.springframework.test.web.client.ResponseCreator;
26+
import org.springframework.util.Assert;
27+
import org.springframework.util.StreamUtils;
28+
29+
/**
30+
* A {@code ResponseCreator} which delegates to a {@link ClientHttpRequestFactory}
31+
* to perform the request and return the associated response.
32+
* This is notably useful when testing code that calls multiple remote services, some
33+
* of which need to be actually called rather than further mocked.
34+
* <p>Note that the input request is asserted to be a {@code MockClientHttpRequest} and
35+
* the URI, method, headers and body are copied.
36+
* <p>The factory can typically be obtained from a {@code RestTemplate} but in case this
37+
* is used with e.g. {@code MockRestServiceServer}, make sure to capture the factory early
38+
* before binding the mock server to the RestTemplate (as it replaces the factory):
39+
* <pre><code>
40+
* ResponseCreator withActualResponse = new ExecutingResponseCreator(restTemplate);
41+
* MockRestServiceServer server = MockRestServiceServer.bindTo(restTemplate).build();
42+
* //...
43+
* server.expect(requestTo("/foo")).andRespond(withSuccess());
44+
* server.expect(requestTo("/bar")).andRespond(withActualResponse);
45+
* </code></pre>
46+
*
47+
* @author Simon Baslé
48+
* @since 6.0.4
49+
*/
50+
public class ExecutingResponseCreator implements ResponseCreator {
51+
52+
private final ClientHttpRequestFactory requestFactory;
53+
54+
55+
/**
56+
* Create a {@code ExecutingResponseCreator} from a {@code ClientHttpRequestFactory}.
57+
* @param requestFactory the request factory to delegate to
58+
*/
59+
public ExecutingResponseCreator(ClientHttpRequestFactory requestFactory) {
60+
this.requestFactory = requestFactory;
61+
}
62+
63+
64+
@Override
65+
public ClientHttpResponse createResponse(ClientHttpRequest request) throws IOException {
66+
Assert.state(request instanceof MockClientHttpRequest, "Request should be an instance of MockClientHttpRequest");
67+
MockClientHttpRequest mockRequest = (MockClientHttpRequest) request;
68+
ClientHttpRequest newRequest = this.requestFactory.createRequest(mockRequest.getURI(), mockRequest.getMethod());
69+
newRequest.getHeaders().putAll(mockRequest.getHeaders());
70+
StreamUtils.copy(mockRequest.getBodyAsBytes(), newRequest.getBody());
71+
return newRequest.execute();
72+
}
73+
}

spring-test/src/main/java/org/springframework/test/web/client/response/MockRestResponseCreators.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,15 @@
3333
* <p><strong>Eclipse users:</strong> consider adding this class as a Java editor
3434
* favorite. To navigate, open the Preferences and type "favorites".
3535
*
36+
* <p>See also {@link ExecutingResponseCreator} for a {@code ResponseCreator} that is
37+
* capable of performing an actual request. That case is not offered as a factory method
38+
* here because of the early setup that is likely needed (capturing a request factory
39+
* which wouldn't be available anymore when the factory methods are typically invoked,
40+
* e.g. replaced in a {@code RestTemplate} by the {@code MockRestServiceServer}).
41+
*
3642
* @author Rossen Stoyanchev
3743
* @since 3.2
44+
* @see ExecutingResponseCreator
3845
*/
3946
public abstract class MockRestResponseCreators {
4047

spring-test/src/test/java/org/springframework/test/web/client/MockRestServiceServerTests.java

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,22 @@
1717
package org.springframework.test.web.client;
1818

1919
import java.net.SocketException;
20+
import java.nio.charset.StandardCharsets;
2021
import java.time.Duration;
2122

2223
import org.junit.jupiter.api.Test;
2324

25+
import org.springframework.http.HttpStatus;
26+
import org.springframework.http.MediaType;
27+
import org.springframework.http.client.ClientHttpRequestFactory;
28+
import org.springframework.http.client.ClientHttpResponse;
29+
import org.springframework.mock.http.client.MockClientHttpRequest;
30+
import org.springframework.mock.http.client.MockClientHttpResponse;
2431
import org.springframework.test.web.client.MockRestServiceServer.MockRestServiceServerBuilder;
32+
import org.springframework.test.web.client.response.ExecutingResponseCreator;
2533
import org.springframework.web.client.RestTemplate;
2634

35+
import static org.assertj.core.api.Assertions.assertThat;
2736
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
2837
import static org.assertj.core.api.Assertions.assertThatThrownBy;
2938
import static org.assertj.core.api.Assertions.fail;
@@ -88,6 +97,43 @@ void ignoreExpectOrder() {
8897
server.verify();
8998
}
9099

100+
@Test
101+
void executingResponseCreator() {
102+
RestTemplate restTemplateWithMockEcho = createEchoRestTemplate();
103+
104+
final ExecutingResponseCreator withActualCall = new ExecutingResponseCreator(restTemplateWithMockEcho.getRequestFactory());
105+
MockRestServiceServer server = MockRestServiceServer.bindTo(restTemplateWithMockEcho).build();
106+
server.expect(requestTo("/profile")).andRespond(withSuccess());
107+
server.expect(requestTo("/quoteOfTheDay")).andRespond(withActualCall);
108+
109+
var response1 = restTemplateWithMockEcho.getForEntity("/profile", String.class);
110+
var response2 = restTemplateWithMockEcho.getForEntity("/quoteOfTheDay", String.class);
111+
server.verify();
112+
113+
assertThat(response1.getStatusCode().value())
114+
.as("response1 status").isEqualTo(200);
115+
assertThat(response1.getBody())
116+
.as("response1 body").isNullOrEmpty();
117+
assertThat(response2.getStatusCode().value())
118+
.as("response2 status").isEqualTo(300);
119+
assertThat(response2.getBody())
120+
.as("response2 body").isEqualTo("echo from /quoteOfTheDay");
121+
}
122+
123+
private static RestTemplate createEchoRestTemplate() {
124+
final ClientHttpRequestFactory echoRequestFactory = (uri, httpMethod) -> {
125+
final MockClientHttpRequest req = new MockClientHttpRequest(httpMethod, uri);
126+
String body = "echo from " + uri.getPath();
127+
final ClientHttpResponse resp = new MockClientHttpResponse(body.getBytes(StandardCharsets.UTF_8),
128+
// Instead of 200, we use a less-common status code on purpose
129+
HttpStatus.MULTIPLE_CHOICES);
130+
resp.getHeaders().setContentType(MediaType.TEXT_PLAIN);
131+
req.setResponse(resp);
132+
return req;
133+
};
134+
return new RestTemplate(echoRequestFactory);
135+
}
136+
91137
@Test
92138
void resetAndReuseServer() {
93139
MockRestServiceServer server = MockRestServiceServer.bindTo(this.restTemplate).build();
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/*
2+
* Copyright 2002-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.test.web.client.response;
18+
19+
import java.io.IOException;
20+
import java.io.OutputStream;
21+
import java.net.URI;
22+
import java.nio.charset.StandardCharsets;
23+
import java.util.ArrayList;
24+
import java.util.List;
25+
26+
import org.junit.jupiter.api.Test;
27+
28+
import org.springframework.http.HttpHeaders;
29+
import org.springframework.http.HttpMethod;
30+
import org.springframework.http.client.AbstractClientHttpRequest;
31+
import org.springframework.http.client.ClientHttpRequest;
32+
import org.springframework.http.client.ClientHttpRequestFactory;
33+
import org.springframework.http.client.ClientHttpResponse;
34+
import org.springframework.mock.http.client.MockClientHttpRequest;
35+
import org.springframework.mock.http.client.MockClientHttpResponse;
36+
37+
import static org.assertj.core.api.Assertions.assertThat;
38+
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
39+
40+
/**
41+
* Tests for the {@link ExecutingResponseCreator} implementation.
42+
*
43+
* @author Simon Baslé
44+
*/
45+
class ExecutingResponseCreatorTests {
46+
47+
@Test
48+
void ensureRequestNotNull() {
49+
final ExecutingResponseCreator responseCreator = new ExecutingResponseCreator((uri, method) -> null);
50+
51+
assertThatIllegalStateException()
52+
.isThrownBy(() -> responseCreator.createResponse(null))
53+
.withMessage("Request should be an instance of MockClientHttpRequest");
54+
}
55+
56+
@Test
57+
void ensureRequestIsMock() {
58+
final ExecutingResponseCreator responseCreator = new ExecutingResponseCreator((uri, method) -> null);
59+
ClientHttpRequest notAMockRequest = new AbstractClientHttpRequest() {
60+
@Override
61+
protected OutputStream getBodyInternal(HttpHeaders headers) throws IOException {
62+
return null;
63+
}
64+
65+
@Override
66+
protected ClientHttpResponse executeInternal(HttpHeaders headers) throws IOException {
67+
return null;
68+
}
69+
70+
@Override
71+
public HttpMethod getMethod() {
72+
return null;
73+
}
74+
75+
@Override
76+
public URI getURI() {
77+
return null;
78+
}
79+
};
80+
81+
assertThatIllegalStateException()
82+
.isThrownBy(() -> responseCreator.createResponse(notAMockRequest))
83+
.withMessage("Request should be an instance of MockClientHttpRequest");
84+
}
85+
86+
@Test
87+
void requestIsCopied() throws IOException {
88+
MockClientHttpRequest originalRequest = new MockClientHttpRequest(HttpMethod.POST,
89+
"https://example.org");
90+
String body = "original body";
91+
originalRequest.getHeaders().add("X-example", "original");
92+
originalRequest.getBody().write(body.getBytes(StandardCharsets.UTF_8));
93+
MockClientHttpResponse originalResponse = new MockClientHttpResponse(new byte[0], 500);
94+
List<MockClientHttpRequest> factoryRequests = new ArrayList<>();
95+
ClientHttpRequestFactory originalFactory = (uri, httpMethod) -> {
96+
MockClientHttpRequest request = new MockClientHttpRequest(httpMethod, uri);
97+
request.setResponse(originalResponse);
98+
factoryRequests.add(request);
99+
return request;
100+
};
101+
102+
final ExecutingResponseCreator responseCreator = new ExecutingResponseCreator(originalFactory);
103+
final ClientHttpResponse response = responseCreator.createResponse(originalRequest);
104+
105+
assertThat(response).as("response").isSameAs(originalResponse);
106+
assertThat(originalRequest.isExecuted()).as("originalRequest.isExecuted").isFalse();
107+
108+
assertThat(factoryRequests)
109+
.hasSize(1)
110+
.first()
111+
.isNotSameAs(originalRequest)
112+
.satisfies(copiedRequest -> {
113+
assertThat(copiedRequest)
114+
.as("copied request")
115+
.isNotSameAs(originalRequest);
116+
assertThat(copiedRequest.isExecuted())
117+
.as("copiedRequest.isExecuted").isTrue();
118+
assertThat(copiedRequest.getBody())
119+
.as("copiedRequest.body").isNotSameAs(originalRequest.getBody());
120+
assertThat(copiedRequest.getHeaders())
121+
.as("copiedRequest.headers").isNotSameAs(originalRequest.getHeaders());
122+
});
123+
}
124+
}

0 commit comments

Comments
 (0)