Add ExecutingResponseCreator
This commit adds a new `ResponseCreator` implementation that uses a `ClientHttpRequestFactory` to perform an actual request. Closes gh-29721
This commit is contained in:
parent
ae7cff35dc
commit
4bcc24372a
|
|
@ -117,6 +117,56 @@ logic but without running a server. The following example shows how to do so:
|
|||
// Test code that uses the above RestTemplate ...
|
||||
----
|
||||
|
||||
In the more specific cases where total isolation isn't desired and some integration testing
|
||||
of one or more calls is needed, a specific `ResponseCreator` can be set up in advance and
|
||||
used to perform actual requests and assert the response.
|
||||
The following example shows how to set up and use the `ExecutingResponseCreator` to do so:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
|
||||
// Make sure to capture the request factory of the RestTemplate before binding
|
||||
ExecutingResponseCreator withActualResponse = new ExecutingResponseCreator(restTemplate.getRequestFactory());
|
||||
|
||||
MockRestServiceServer mockServer = MockRestServiceServer.bindTo(restTemplate).build();
|
||||
mockServer.expect(requestTo("/greeting")).andRespond(withActualResponse);
|
||||
|
||||
// Test code that uses the above RestTemplate ...
|
||||
|
||||
mockServer.verify();
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val restTemplate = RestTemplate()
|
||||
|
||||
// Make sure to capture the request factory of the RestTemplate before binding
|
||||
val withActualResponse = new ExecutingResponseCreator(restTemplate.getRequestFactory())
|
||||
|
||||
val mockServer = MockRestServiceServer.bindTo(restTemplate).build()
|
||||
mockServer.expect(requestTo("/profile")).andRespond(withSuccess())
|
||||
mockServer.expect(requestTo("/quoteOfTheDay")).andRespond(withActualResponse)
|
||||
|
||||
// Test code that uses the above RestTemplate ...
|
||||
|
||||
mockServer.verify()
|
||||
----
|
||||
|
||||
In the preceding example, we create the `ExecutingResponseCreator` using the
|
||||
`ClientHttpRequestFactory` from the `RestTemplate` _before_ `MockRestServiceServer` replaces
|
||||
it with the custom one.
|
||||
Then we define expectations with two kinds of response:
|
||||
|
||||
* a stub `200` response for the `/profile` endpoint (no actual request will be executed)
|
||||
* an "executing response" for the `/quoteOfTheDay` endpoint
|
||||
|
||||
In the second case, the request is executed by the `ClientHttpRequestFactory` that was
|
||||
captured earlier. This generates a response that could e.g. come from an actual remote server,
|
||||
depending on how the `RestTemplate` was originally configured, and MockMVC can be further
|
||||
used to assert the content of the response.
|
||||
|
||||
[[spring-mvc-test-client-static-imports]]
|
||||
== Static Imports
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* Copyright 2002-2023 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.test.web.client.response;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.springframework.http.client.ClientHttpRequest;
|
||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.mock.http.client.MockClientHttpRequest;
|
||||
import org.springframework.test.web.client.ResponseCreator;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StreamUtils;
|
||||
|
||||
/**
|
||||
* A {@code ResponseCreator} which delegates to a {@link ClientHttpRequestFactory}
|
||||
* to perform the request and return the associated response.
|
||||
* This is notably useful when testing code that calls multiple remote services, some
|
||||
* of which need to be actually called rather than further mocked.
|
||||
* <p>Note that the input request is asserted to be a {@code MockClientHttpRequest} and
|
||||
* the URI, method, headers and body are copied.
|
||||
* <p>The factory can typically be obtained from a {@code RestTemplate} but in case this
|
||||
* is used with e.g. {@code MockRestServiceServer}, make sure to capture the factory early
|
||||
* before binding the mock server to the RestTemplate (as it replaces the factory):
|
||||
* <pre><code>
|
||||
* ResponseCreator withActualResponse = new ExecutingResponseCreator(restTemplate);
|
||||
* MockRestServiceServer server = MockRestServiceServer.bindTo(restTemplate).build();
|
||||
* //...
|
||||
* server.expect(requestTo("/foo")).andRespond(withSuccess());
|
||||
* server.expect(requestTo("/bar")).andRespond(withActualResponse);
|
||||
* </code></pre>
|
||||
*
|
||||
* @author Simon Baslé
|
||||
* @since 6.0.4
|
||||
*/
|
||||
public class ExecutingResponseCreator implements ResponseCreator {
|
||||
|
||||
private final ClientHttpRequestFactory requestFactory;
|
||||
|
||||
|
||||
/**
|
||||
* Create a {@code ExecutingResponseCreator} from a {@code ClientHttpRequestFactory}.
|
||||
* @param requestFactory the request factory to delegate to
|
||||
*/
|
||||
public ExecutingResponseCreator(ClientHttpRequestFactory requestFactory) {
|
||||
this.requestFactory = requestFactory;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public ClientHttpResponse createResponse(ClientHttpRequest request) throws IOException {
|
||||
Assert.state(request instanceof MockClientHttpRequest, "Request should be an instance of MockClientHttpRequest");
|
||||
MockClientHttpRequest mockRequest = (MockClientHttpRequest) request;
|
||||
ClientHttpRequest newRequest = this.requestFactory.createRequest(mockRequest.getURI(), mockRequest.getMethod());
|
||||
newRequest.getHeaders().putAll(mockRequest.getHeaders());
|
||||
StreamUtils.copy(mockRequest.getBodyAsBytes(), newRequest.getBody());
|
||||
return newRequest.execute();
|
||||
}
|
||||
}
|
||||
|
|
@ -33,8 +33,15 @@ import org.springframework.test.web.client.ResponseCreator;
|
|||
* <p><strong>Eclipse users:</strong> consider adding this class as a Java editor
|
||||
* favorite. To navigate, open the Preferences and type "favorites".
|
||||
*
|
||||
* <p>See also {@link ExecutingResponseCreator} for a {@code ResponseCreator} that is
|
||||
* capable of performing an actual request. That case is not offered as a factory method
|
||||
* here because of the early setup that is likely needed (capturing a request factory
|
||||
* which wouldn't be available anymore when the factory methods are typically invoked,
|
||||
* e.g. replaced in a {@code RestTemplate} by the {@code MockRestServiceServer}).
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 3.2
|
||||
* @see ExecutingResponseCreator
|
||||
*/
|
||||
public abstract class MockRestResponseCreators {
|
||||
|
||||
|
|
|
|||
|
|
@ -17,13 +17,22 @@
|
|||
package org.springframework.test.web.client;
|
||||
|
||||
import java.net.SocketException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.mock.http.client.MockClientHttpRequest;
|
||||
import org.springframework.mock.http.client.MockClientHttpResponse;
|
||||
import org.springframework.test.web.client.MockRestServiceServer.MockRestServiceServerBuilder;
|
||||
import org.springframework.test.web.client.response.ExecutingResponseCreator;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.assertj.core.api.Assertions.fail;
|
||||
|
|
@ -88,6 +97,43 @@ class MockRestServiceServerTests {
|
|||
server.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
void executingResponseCreator() {
|
||||
RestTemplate restTemplateWithMockEcho = createEchoRestTemplate();
|
||||
|
||||
final ExecutingResponseCreator withActualCall = new ExecutingResponseCreator(restTemplateWithMockEcho.getRequestFactory());
|
||||
MockRestServiceServer server = MockRestServiceServer.bindTo(restTemplateWithMockEcho).build();
|
||||
server.expect(requestTo("/profile")).andRespond(withSuccess());
|
||||
server.expect(requestTo("/quoteOfTheDay")).andRespond(withActualCall);
|
||||
|
||||
var response1 = restTemplateWithMockEcho.getForEntity("/profile", String.class);
|
||||
var response2 = restTemplateWithMockEcho.getForEntity("/quoteOfTheDay", String.class);
|
||||
server.verify();
|
||||
|
||||
assertThat(response1.getStatusCode().value())
|
||||
.as("response1 status").isEqualTo(200);
|
||||
assertThat(response1.getBody())
|
||||
.as("response1 body").isNullOrEmpty();
|
||||
assertThat(response2.getStatusCode().value())
|
||||
.as("response2 status").isEqualTo(300);
|
||||
assertThat(response2.getBody())
|
||||
.as("response2 body").isEqualTo("echo from /quoteOfTheDay");
|
||||
}
|
||||
|
||||
private static RestTemplate createEchoRestTemplate() {
|
||||
final ClientHttpRequestFactory echoRequestFactory = (uri, httpMethod) -> {
|
||||
final MockClientHttpRequest req = new MockClientHttpRequest(httpMethod, uri);
|
||||
String body = "echo from " + uri.getPath();
|
||||
final ClientHttpResponse resp = new MockClientHttpResponse(body.getBytes(StandardCharsets.UTF_8),
|
||||
// Instead of 200, we use a less-common status code on purpose
|
||||
HttpStatus.MULTIPLE_CHOICES);
|
||||
resp.getHeaders().setContentType(MediaType.TEXT_PLAIN);
|
||||
req.setResponse(resp);
|
||||
return req;
|
||||
};
|
||||
return new RestTemplate(echoRequestFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resetAndReuseServer() {
|
||||
MockRestServiceServer server = MockRestServiceServer.bindTo(this.restTemplate).build();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,124 @@
|
|||
/*
|
||||
* Copyright 2002-2023 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.test.web.client.response;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.client.AbstractClientHttpRequest;
|
||||
import org.springframework.http.client.ClientHttpRequest;
|
||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.mock.http.client.MockClientHttpRequest;
|
||||
import org.springframework.mock.http.client.MockClientHttpResponse;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
|
||||
/**
|
||||
* Tests for the {@link ExecutingResponseCreator} implementation.
|
||||
*
|
||||
* @author Simon Baslé
|
||||
*/
|
||||
class ExecutingResponseCreatorTests {
|
||||
|
||||
@Test
|
||||
void ensureRequestNotNull() {
|
||||
final ExecutingResponseCreator responseCreator = new ExecutingResponseCreator((uri, method) -> null);
|
||||
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> responseCreator.createResponse(null))
|
||||
.withMessage("Request should be an instance of MockClientHttpRequest");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ensureRequestIsMock() {
|
||||
final ExecutingResponseCreator responseCreator = new ExecutingResponseCreator((uri, method) -> null);
|
||||
ClientHttpRequest notAMockRequest = new AbstractClientHttpRequest() {
|
||||
@Override
|
||||
protected OutputStream getBodyInternal(HttpHeaders headers) throws IOException {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ClientHttpResponse executeInternal(HttpHeaders headers) throws IOException {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpMethod getMethod() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public URI getURI() {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> responseCreator.createResponse(notAMockRequest))
|
||||
.withMessage("Request should be an instance of MockClientHttpRequest");
|
||||
}
|
||||
|
||||
@Test
|
||||
void requestIsCopied() throws IOException {
|
||||
MockClientHttpRequest originalRequest = new MockClientHttpRequest(HttpMethod.POST,
|
||||
"https://example.org");
|
||||
String body = "original body";
|
||||
originalRequest.getHeaders().add("X-example", "original");
|
||||
originalRequest.getBody().write(body.getBytes(StandardCharsets.UTF_8));
|
||||
MockClientHttpResponse originalResponse = new MockClientHttpResponse(new byte[0], 500);
|
||||
List<MockClientHttpRequest> factoryRequests = new ArrayList<>();
|
||||
ClientHttpRequestFactory originalFactory = (uri, httpMethod) -> {
|
||||
MockClientHttpRequest request = new MockClientHttpRequest(httpMethod, uri);
|
||||
request.setResponse(originalResponse);
|
||||
factoryRequests.add(request);
|
||||
return request;
|
||||
};
|
||||
|
||||
final ExecutingResponseCreator responseCreator = new ExecutingResponseCreator(originalFactory);
|
||||
final ClientHttpResponse response = responseCreator.createResponse(originalRequest);
|
||||
|
||||
assertThat(response).as("response").isSameAs(originalResponse);
|
||||
assertThat(originalRequest.isExecuted()).as("originalRequest.isExecuted").isFalse();
|
||||
|
||||
assertThat(factoryRequests)
|
||||
.hasSize(1)
|
||||
.first()
|
||||
.isNotSameAs(originalRequest)
|
||||
.satisfies(copiedRequest -> {
|
||||
assertThat(copiedRequest)
|
||||
.as("copied request")
|
||||
.isNotSameAs(originalRequest);
|
||||
assertThat(copiedRequest.isExecuted())
|
||||
.as("copiedRequest.isExecuted").isTrue();
|
||||
assertThat(copiedRequest.getBody())
|
||||
.as("copiedRequest.body").isNotSameAs(originalRequest.getBody());
|
||||
assertThat(copiedRequest.getHeaders())
|
||||
.as("copiedRequest.headers").isNotSameAs(originalRequest.getHeaders());
|
||||
});
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue