Polishing

See gh-29721
This commit is contained in:
rstoyanchev 2023-01-06 16:50:19 +00:00
parent 4bcc24372a
commit c702cd418e
5 changed files with 59 additions and 102 deletions

View File

@ -117,21 +117,21 @@ logic but without running a server. The following example shows how to do so:
// Test code that uses the above RestTemplate ... // Test code that uses the above RestTemplate ...
---- ----
In the more specific cases where total isolation isn't desired and some integration testing In some cases it may be necessary to perform an actual call to a remote service instead
of one or more calls is needed, a specific `ResponseCreator` can be set up in advance and of mocking the response. The following example shows how to do that through
used to perform actual requests and assert the response. `ExecutingResponseCreator`:
The following example shows how to set up and use the `ExecutingResponseCreator` to do so:
[source,java,indent=0,subs="verbatim,quotes",role="primary"] [source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java .Java
---- ----
RestTemplate restTemplate = new RestTemplate(); RestTemplate restTemplate = new RestTemplate();
// Make sure to capture the request factory of the RestTemplate before binding // Create ExecutingResponseCreator with the original request factory
ExecutingResponseCreator withActualResponse = new ExecutingResponseCreator(restTemplate.getRequestFactory()); ExecutingResponseCreator withActualResponse = new ExecutingResponseCreator(restTemplate.getRequestFactory());
MockRestServiceServer mockServer = MockRestServiceServer.bindTo(restTemplate).build(); MockRestServiceServer mockServer = MockRestServiceServer.bindTo(restTemplate).build();
mockServer.expect(requestTo("/greeting")).andRespond(withActualResponse); mockServer.expect(requestTo("/profile")).andRespond(withSuccess());
mockServer.expect(requestTo("/quoteOfTheDay")).andRespond(withActualResponse);
// Test code that uses the above RestTemplate ... // Test code that uses the above RestTemplate ...
@ -142,7 +142,7 @@ The following example shows how to set up and use the `ExecutingResponseCreator`
---- ----
val restTemplate = RestTemplate() val restTemplate = RestTemplate()
// Make sure to capture the request factory of the RestTemplate before binding // Create ExecutingResponseCreator with the original request factory
val withActualResponse = new ExecutingResponseCreator(restTemplate.getRequestFactory()) val withActualResponse = new ExecutingResponseCreator(restTemplate.getRequestFactory())
val mockServer = MockRestServiceServer.bindTo(restTemplate).build() val mockServer = MockRestServiceServer.bindTo(restTemplate).build()
@ -156,16 +156,15 @@ The following example shows how to set up and use the `ExecutingResponseCreator`
In the preceding example, we create the `ExecutingResponseCreator` using the In the preceding example, we create the `ExecutingResponseCreator` using the
`ClientHttpRequestFactory` from the `RestTemplate` _before_ `MockRestServiceServer` replaces `ClientHttpRequestFactory` from the `RestTemplate` _before_ `MockRestServiceServer` replaces
it with the custom one. it with a different one that mocks responses.
Then we define expectations with two kinds of response: Then we define expectations with two kinds of responses:
* a stub `200` response for the `/profile` endpoint (no actual request will be executed) * a stub `200` response for the `/profile` endpoint (no actual request will be executed)
* an "executing response" for the `/quoteOfTheDay` endpoint * a response obtained through a call to the `/quoteOfTheDay` endpoint
In the second case, the request is executed by the `ClientHttpRequestFactory` that was In the second case, the request is executed through the `ClientHttpRequestFactory` that was
captured earlier. This generates a response that could e.g. come from an actual remote server, 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 depending on how the `RestTemplate` was originally configured.
used to assert the content of the response.
[[spring-mvc-test-client-static-imports]] [[spring-mvc-test-client-static-imports]]
== Static Imports == Static Imports

View File

@ -27,15 +27,12 @@ import org.springframework.util.Assert;
import org.springframework.util.StreamUtils; import org.springframework.util.StreamUtils;
/** /**
* A {@code ResponseCreator} which delegates to a {@link ClientHttpRequestFactory} * {@code ResponseCreator} that obtains the response by executing the request
* to perform the request and return the associated response. * through a {@link ClientHttpRequestFactory}. This is useful in scenarios with
* This is notably useful when testing code that calls multiple remote services, some * multiple remote services where some need to be called rather than mocked.
* of which need to be actually called rather than further mocked. * <p>The {@code ClientHttpRequestFactory} is typically obtained from the
* <p>Note that the input request is asserted to be a {@code MockClientHttpRequest} and * {@code RestTemplate} before it is passed to {@code MockRestServiceServer},
* the URI, method, headers and body are copied. * in effect using the original factory rather than the test factory:
* <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> * <pre><code>
* ResponseCreator withActualResponse = new ExecutingResponseCreator(restTemplate); * ResponseCreator withActualResponse = new ExecutingResponseCreator(restTemplate);
* MockRestServiceServer server = MockRestServiceServer.bindTo(restTemplate).build(); * MockRestServiceServer server = MockRestServiceServer.bindTo(restTemplate).build();
@ -53,7 +50,7 @@ public class ExecutingResponseCreator implements ResponseCreator {
/** /**
* Create a {@code ExecutingResponseCreator} from a {@code ClientHttpRequestFactory}. * Create an instance with the given {@code ClientHttpRequestFactory}.
* @param requestFactory the request factory to delegate to * @param requestFactory the request factory to delegate to
*/ */
public ExecutingResponseCreator(ClientHttpRequestFactory requestFactory) { public ExecutingResponseCreator(ClientHttpRequestFactory requestFactory) {
@ -63,7 +60,7 @@ public class ExecutingResponseCreator implements ResponseCreator {
@Override @Override
public ClientHttpResponse createResponse(ClientHttpRequest request) throws IOException { public ClientHttpResponse createResponse(ClientHttpRequest request) throws IOException {
Assert.state(request instanceof MockClientHttpRequest, "Request should be an instance of MockClientHttpRequest"); Assert.state(request instanceof MockClientHttpRequest, "Expected a MockClientHttpRequest");
MockClientHttpRequest mockRequest = (MockClientHttpRequest) request; MockClientHttpRequest mockRequest = (MockClientHttpRequest) request;
ClientHttpRequest newRequest = this.requestFactory.createRequest(mockRequest.getURI(), mockRequest.getMethod()); ClientHttpRequest newRequest = this.requestFactory.createRequest(mockRequest.getURI(), mockRequest.getMethod());
newRequest.getHeaders().putAll(mockRequest.getHeaders()); newRequest.getHeaders().putAll(mockRequest.getHeaders());

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2022 the original author or authors. * Copyright 2002-2023 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -28,16 +28,11 @@ import org.springframework.lang.Nullable;
import org.springframework.test.web.client.ResponseCreator; import org.springframework.test.web.client.ResponseCreator;
/** /**
* Static factory methods for obtaining a {@link ResponseCreator} instance. * Static factory methods to obtain a {@link ResponseCreator} with a fixed
* response.
* *
* <p><strong>Eclipse users:</strong> consider adding this class as a Java editor * <p>In addition, see also the {@link ExecutingResponseCreator} implementation
* favorite. To navigate, open the Preferences and type "favorites". * that performs an actual requests to remote services.
*
* <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 * @author Rossen Stoyanchev
* @since 3.2 * @since 3.2

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2022 the original author or authors. * Copyright 2002-2023 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -99,39 +99,34 @@ class MockRestServiceServerTests {
@Test @Test
void executingResponseCreator() { void executingResponseCreator() {
RestTemplate restTemplateWithMockEcho = createEchoRestTemplate(); RestTemplate restTemplate = createEchoRestTemplate();
ExecutingResponseCreator withActualCall = new ExecutingResponseCreator(restTemplate.getRequestFactory());
final ExecutingResponseCreator withActualCall = new ExecutingResponseCreator(restTemplateWithMockEcho.getRequestFactory()); MockRestServiceServer server = MockRestServiceServer.bindTo(restTemplate).build();
MockRestServiceServer server = MockRestServiceServer.bindTo(restTemplateWithMockEcho).build();
server.expect(requestTo("/profile")).andRespond(withSuccess()); server.expect(requestTo("/profile")).andRespond(withSuccess());
server.expect(requestTo("/quoteOfTheDay")).andRespond(withActualCall); server.expect(requestTo("/quoteOfTheDay")).andRespond(withActualCall);
var response1 = restTemplateWithMockEcho.getForEntity("/profile", String.class); var response1 = restTemplate.getForEntity("/profile", String.class);
var response2 = restTemplateWithMockEcho.getForEntity("/quoteOfTheDay", String.class); var response2 = restTemplate.getForEntity("/quoteOfTheDay", String.class);
server.verify(); server.verify();
assertThat(response1.getStatusCode().value()) assertThat(response1.getStatusCode().value()).isEqualTo(200);
.as("response1 status").isEqualTo(200); assertThat(response1.getBody()).isNullOrEmpty();
assertThat(response1.getBody()) assertThat(response2.getStatusCode().value()).isEqualTo(300);
.as("response1 body").isNullOrEmpty(); assertThat(response2.getBody()).isEqualTo("echo from /quoteOfTheDay");
assertThat(response2.getStatusCode().value())
.as("response2 status").isEqualTo(300);
assertThat(response2.getBody())
.as("response2 body").isEqualTo("echo from /quoteOfTheDay");
} }
private static RestTemplate createEchoRestTemplate() { private static RestTemplate createEchoRestTemplate() {
final ClientHttpRequestFactory echoRequestFactory = (uri, httpMethod) -> { ClientHttpRequestFactory requestFactory = (uri, httpMethod) -> {
final MockClientHttpRequest req = new MockClientHttpRequest(httpMethod, uri); MockClientHttpRequest request = new MockClientHttpRequest(httpMethod, uri);
String body = "echo from " + uri.getPath(); ClientHttpResponse response = new MockClientHttpResponse(
final ClientHttpResponse resp = new MockClientHttpResponse(body.getBytes(StandardCharsets.UTF_8), ("echo from " + uri.getPath()).getBytes(StandardCharsets.UTF_8),
// Instead of 200, we use a less-common status code on purpose HttpStatus.MULTIPLE_CHOICES); // use a different status on purpose
HttpStatus.MULTIPLE_CHOICES); response.getHeaders().setContentType(MediaType.TEXT_PLAIN);
resp.getHeaders().setContentType(MediaType.TEXT_PLAIN); request.setResponse(response);
req.setResponse(resp); return request;
return req;
}; };
return new RestTemplate(echoRequestFactory); return new RestTemplate(requestFactory);
} }
@Test @Test

View File

@ -17,17 +17,13 @@
package org.springframework.test.web.client.response; package org.springframework.test.web.client.response;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.http.client.AbstractClientHttpRequest;
import org.springframework.http.client.ClientHttpRequest; import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.ClientHttpResponse; import org.springframework.http.client.ClientHttpResponse;
@ -36,6 +32,7 @@ import org.springframework.mock.http.client.MockClientHttpResponse;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.Mockito.mock;
/** /**
* Tests for the {@link ExecutingResponseCreator} implementation. * Tests for the {@link ExecutingResponseCreator} implementation.
@ -46,50 +43,29 @@ class ExecutingResponseCreatorTests {
@Test @Test
void ensureRequestNotNull() { void ensureRequestNotNull() {
final ExecutingResponseCreator responseCreator = new ExecutingResponseCreator((uri, method) -> null); ExecutingResponseCreator responseCreator = new ExecutingResponseCreator((uri, method) -> null);
assertThatIllegalStateException() assertThatIllegalStateException()
.isThrownBy(() -> responseCreator.createResponse(null)) .isThrownBy(() -> responseCreator.createResponse(null))
.withMessage("Request should be an instance of MockClientHttpRequest"); .withMessage("Expected a MockClientHttpRequest");
} }
@Test @Test
void ensureRequestIsMock() { void ensureRequestIsMock() {
final ExecutingResponseCreator responseCreator = new ExecutingResponseCreator((uri, method) -> null); final ExecutingResponseCreator responseCreator = new ExecutingResponseCreator((uri, method) -> null);
ClientHttpRequest notAMockRequest = new AbstractClientHttpRequest() { ClientHttpRequest notAMockRequest = mock(ClientHttpRequest.class);
@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() assertThatIllegalStateException()
.isThrownBy(() -> responseCreator.createResponse(notAMockRequest)) .isThrownBy(() -> responseCreator.createResponse(notAMockRequest))
.withMessage("Request should be an instance of MockClientHttpRequest"); .withMessage("Expected a MockClientHttpRequest");
} }
@Test @Test
void requestIsCopied() throws IOException { void requestIsCopied() throws IOException {
MockClientHttpRequest originalRequest = new MockClientHttpRequest(HttpMethod.POST, MockClientHttpRequest originalRequest = new MockClientHttpRequest(HttpMethod.POST, "https://example.org");
"https://example.org");
String body = "original body";
originalRequest.getHeaders().add("X-example", "original"); originalRequest.getHeaders().add("X-example", "original");
originalRequest.getBody().write(body.getBytes(StandardCharsets.UTF_8)); originalRequest.getBody().write("original body".getBytes(StandardCharsets.UTF_8));
MockClientHttpResponse originalResponse = new MockClientHttpResponse(new byte[0], 500); MockClientHttpResponse originalResponse = new MockClientHttpResponse(new byte[0], 500);
List<MockClientHttpRequest> factoryRequests = new ArrayList<>(); List<MockClientHttpRequest> factoryRequests = new ArrayList<>();
ClientHttpRequestFactory originalFactory = (uri, httpMethod) -> { ClientHttpRequestFactory originalFactory = (uri, httpMethod) -> {
@ -99,8 +75,8 @@ class ExecutingResponseCreatorTests {
return request; return request;
}; };
final ExecutingResponseCreator responseCreator = new ExecutingResponseCreator(originalFactory); ExecutingResponseCreator responseCreator = new ExecutingResponseCreator(originalFactory);
final ClientHttpResponse response = responseCreator.createResponse(originalRequest); ClientHttpResponse response = responseCreator.createResponse(originalRequest);
assertThat(response).as("response").isSameAs(originalResponse); assertThat(response).as("response").isSameAs(originalResponse);
assertThat(originalRequest.isExecuted()).as("originalRequest.isExecuted").isFalse(); assertThat(originalRequest.isExecuted()).as("originalRequest.isExecuted").isFalse();
@ -109,16 +85,11 @@ class ExecutingResponseCreatorTests {
.hasSize(1) .hasSize(1)
.first() .first()
.isNotSameAs(originalRequest) .isNotSameAs(originalRequest)
.satisfies(copiedRequest -> { .satisfies(request -> {
assertThat(copiedRequest) assertThat(request).isNotSameAs(originalRequest);
.as("copied request") assertThat(request.isExecuted()).isTrue();
.isNotSameAs(originalRequest); assertThat(request.getBody()).isNotSameAs(originalRequest.getBody());
assertThat(copiedRequest.isExecuted()) assertThat(request.getHeaders()).isNotSameAs(originalRequest.getHeaders());
.as("copiedRequest.isExecuted").isTrue();
assertThat(copiedRequest.getBody())
.as("copiedRequest.body").isNotSameAs(originalRequest.getBody());
assertThat(copiedRequest.getHeaders())
.as("copiedRequest.headers").isNotSameAs(originalRequest.getHeaders());
}); });
} }
} }