diff --git a/spring-test/src/main/java/org/springframework/test/web/client/AbstractRequestExpectationManager.java b/spring-test/src/main/java/org/springframework/test/web/client/AbstractRequestExpectationManager.java index de40942c33..3591784ce4 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/AbstractRequestExpectationManager.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/AbstractRequestExpectationManager.java @@ -16,19 +16,25 @@ package org.springframework.test.web.client; import java.io.IOException; +import java.net.URI; +import java.util.Collection; +import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; +import java.util.Set; +import org.springframework.http.HttpMethod; import org.springframework.http.client.ClientHttpRequest; import org.springframework.http.client.ClientHttpResponse; import org.springframework.util.Assert; /** * Base class for {@code RequestExpectationManager} implementations responsible - * for storing expectations and requests. + * for storing expectations and actual requests, and checking for unsatisfied + * expectations at the end. * - *

Sub-classes are responsible for matching requests to expectations and - * verifying there are no remaining expectations at the end. + *

Sub-classes are responsible for validating each request by matching it to + * to expectations following the order of declaration or not. * * @author Rossen Stoyanchev * @since 4.3 @@ -48,26 +54,127 @@ public abstract class AbstractRequestExpectationManager implements RequestExpect return this.requests; } + @Override - public ResponseActions expectRequest(RequestMatcher requestMatcher) { + public ResponseActions expectRequest(ExpectedCount count, RequestMatcher matcher) { Assert.state(getRequests().isEmpty(), "Cannot add more expectations after actual requests are made."); - RequestExpectation expectation = createExpectation(requestMatcher); + RequestExpectation expectation = new DefaultRequestExpectation(count, matcher); getExpectations().add(expectation); return expectation; } - protected RequestExpectation createExpectation(RequestMatcher requestMatcher) { - return new DefaultRequestExpectation(requestMatcher); - } - @Override public ClientHttpResponse validateRequest(ClientHttpRequest request) throws IOException { + if (getRequests().isEmpty()) { + afterExpectationsDeclared(); + } ClientHttpResponse response = validateRequestInternal(request); getRequests().add(request); return response; } + /** + * Invoked after the phase of declaring expected requests is over. This is + * detected from {@link #validateRequest} on the first actual request. + */ + protected void afterExpectationsDeclared() { + } + + /** + * Sub-classes must implement the actual validation of the request + * matching it to a declared expectation. + */ protected abstract ClientHttpResponse validateRequestInternal(ClientHttpRequest request) throws IOException; + @Override + public void verify() { + if (getExpectations().isEmpty()) { + return; + } + int count = 0; + for (RequestExpectation expectation : getExpectations()) { + if (!expectation.isSatisfied()) { + count++; + } + } + if (count > 0) { + String message = "Further request(s) expected leaving " + count + " unsatisfied expectation(s).\n"; + throw new AssertionError(message + getRequestDetails()); + } + } + + /** + * Return details of executed requests. + */ + protected String getRequestDetails() { + StringBuilder sb = new StringBuilder(); + sb.append(getRequests().size()).append(" request(s) executed"); + if (!getRequests().isEmpty()) { + sb.append(":\n"); + for (ClientHttpRequest request : getRequests()) { + sb.append(request.toString()).append("\n"); + } + } + else { + sb.append(".\n"); + } + return sb.toString(); + } + + /** + * Return an {@code AssertionError} that a sub-class can raise for an + * unexpected request. + */ + protected AssertionError createUnexpectedRequestError(ClientHttpRequest request) { + HttpMethod method = request.getMethod(); + URI uri = request.getURI(); + String message = "No further requests expected: HTTP " + method + " " + uri + "\n"; + return new AssertionError(message + getRequestDetails()); + } + + + /** + * Helper class to manage a group of request expectations. It helps with + * operations against the entire group such as finding a match and updating + * (add or remove) based on expected request count. + */ + protected static class RequestExpectationGroup { + + private final Set expectations = new LinkedHashSet(); + + + public Set getExpectations() { + return this.expectations; + } + + public void update(RequestExpectation expectation) { + if (expectation.hasRemainingCount()) { + getExpectations().add(expectation); + } + else { + getExpectations().remove(expectation); + } + } + + public void updateAll(Collection expectations) { + for (RequestExpectation expectation : expectations) { + update(expectation); + } + } + + public RequestExpectation findExpectation(ClientHttpRequest request) throws IOException { + for (RequestExpectation expectation : getExpectations()) { + try { + expectation.match(request); + return expectation; + } + catch (AssertionError error) { + // Ignore + } + } + return null; + } + } + } diff --git a/spring-test/src/main/java/org/springframework/test/web/client/DefaultRequestExpectation.java b/spring-test/src/main/java/org/springframework/test/web/client/DefaultRequestExpectation.java index 45efae1cf5..7c4950dbee 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/DefaultRequestExpectation.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/DefaultRequestExpectation.java @@ -33,17 +33,38 @@ import org.springframework.util.Assert; */ public class DefaultRequestExpectation implements RequestExpectation { + private final RequestCount requestCount; + private final List requestMatchers = new LinkedList(); private ResponseCreator responseCreator; - public DefaultRequestExpectation(RequestMatcher requestMatcher) { - Assert.notNull(requestMatcher, "RequestMatcher is required"); + /** + * Create a new request expectation that should be called a number of times + * as indicated by {@code RequestCount}. + * @param expectedCount the expected request expectedCount + */ + public DefaultRequestExpectation(ExpectedCount expectedCount, RequestMatcher requestMatcher) { + Assert.notNull(expectedCount, "'expectedCount' is required"); + Assert.notNull(requestMatcher, "'requestMatcher' is required"); + this.requestCount = new RequestCount(expectedCount); this.requestMatchers.add(requestMatcher); } + protected RequestCount getRequestCount() { + return this.requestCount; + } + + protected List getRequestMatchers() { + return this.requestMatchers; + } + + protected ResponseCreator getResponseCreator() { + return this.responseCreator; + } + @Override public ResponseActions andExpect(RequestMatcher requestMatcher) { Assert.notNull(requestMatcher, "RequestMatcher is required"); @@ -59,17 +80,68 @@ public class DefaultRequestExpectation implements RequestExpectation { @Override public void match(ClientHttpRequest request) throws IOException { - for (RequestMatcher matcher : this.requestMatchers) { + for (RequestMatcher matcher : getRequestMatchers()) { matcher.match(request); } } @Override public ClientHttpResponse createResponse(ClientHttpRequest request) throws IOException { - if (this.responseCreator == null) { + if (getResponseCreator() == null) { throw new IllegalStateException("createResponse called before ResponseCreator was set."); } - return this.responseCreator.createResponse(request); + getRequestCount().incrementAndValidate(); + return getResponseCreator().createResponse(request); + } + + @Override + public boolean hasRemainingCount() { + return getRequestCount().hasRemainingCount(); + } + + @Override + public boolean isSatisfied() { + return getRequestCount().isSatisfied(); + } + + + /** + * Helper class that keeps track of actual vs expected request count. + */ + protected static class RequestCount { + + private final ExpectedCount expectedCount; + + private int matchedRequestCount; + + + public RequestCount(ExpectedCount expectedCount) { + this.expectedCount = expectedCount; + } + + + public ExpectedCount getExpectedCount() { + return this.expectedCount; + } + + public int getMatchedRequestCount() { + return this.matchedRequestCount; + } + + public void incrementAndValidate() { + this.matchedRequestCount++; + if (getMatchedRequestCount() > getExpectedCount().getMaxCount()) { + throw new AssertionError("No more calls expected."); + } + } + + public boolean hasRemainingCount() { + return (getMatchedRequestCount() < getExpectedCount().getMaxCount()); + } + + public boolean isSatisfied() { + return (getMatchedRequestCount() >= getExpectedCount().getMinCount()); + } } } diff --git a/spring-test/src/main/java/org/springframework/test/web/client/ExpectedCount.java b/spring-test/src/main/java/org/springframework/test/web/client/ExpectedCount.java new file mode 100644 index 0000000000..b508d6fe75 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/client/ExpectedCount.java @@ -0,0 +1,117 @@ +/* + * Copyright 2002-2016 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 + * + * http://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; + +import org.springframework.util.Assert; + +/** + * A simple type representing a range for an expected count. + * + *

Examples: + *

+ * import static org.springframework.test.web.client.ExpectedCount.*
+ *
+ * once()
+ * manyTimes()
+ * times(5)
+ * min(2)
+ * max(4)
+ * between(2, 4)
+ * 
+ * + * @author Rossen Stoyanchev + * @since 4.3 + */ +public class ExpectedCount { + + private final int minCount; + + private final int maxCount; + + + /** + * Private constructor. + * See static factory methods in this class. + */ + private ExpectedCount(int minCount, int maxCount) { + Assert.isTrue(minCount >= 1, "minCount >= 0 is required"); + Assert.isTrue(maxCount >= minCount, "maxCount >= minCount is required"); + this.minCount = minCount; + this.maxCount = maxCount; + } + + + /** + * Return the {@code min} boundary of the expected count range. + */ + public int getMinCount() { + return this.minCount; + } + + /** + * Return the {@code max} boundary of the expected count range. + */ + public int getMaxCount() { + return this.maxCount; + } + + + /** + * Exactly once. + */ + public static ExpectedCount once() { + return new ExpectedCount(1, 1); + } + + /** + * Many times (range of 1..Integer.MAX_VALUE). + */ + public static ExpectedCount manyTimes() { + return new ExpectedCount(1, Integer.MAX_VALUE); + } + + /** + * Exactly N times. + */ + public static ExpectedCount times(int count) { + Assert.isTrue(count >= 1, "'count' must be >= 1"); + return new ExpectedCount(count, count); + } + + /** + * At least {@code min} number of times. + */ + public static ExpectedCount min(int min) { + Assert.isTrue(min >= 1, "'min' must be >= 1"); + return new ExpectedCount(min, Integer.MAX_VALUE); + } + + /** + * At most {@code max} number of times. + */ + public static ExpectedCount max(int max) { + Assert.isTrue(max >= 1, "'max' must be >= 1"); + return new ExpectedCount(1, max); + } + + /** + * Between {@code min} and {@code max} number of times. + */ + public static ExpectedCount between(int min, int max) { + return new ExpectedCount(min, max); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/web/client/MockRestServiceServer.java b/spring-test/src/main/java/org/springframework/test/web/client/MockRestServiceServer.java index 82ebcd8a80..726bb79179 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/MockRestServiceServer.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/MockRestServiceServer.java @@ -35,59 +35,33 @@ import org.springframework.web.client.support.RestGatewaySupport; /** * Main entry point for client-side REST testing. Used for tests - * that involve direct or indirect (through client code) use of the - * {@link RestTemplate}. Provides a way to set up fine-grained expectations - * on the requests that will be performed through the {@code RestTemplate} and - * a way to define the responses to send back removing the need for an - * actual running server. + * that involve direct or indirect use of the {@link RestTemplate}. Provides a + * way to set up expected requests that will be performed through the + * {@code RestTemplate} as well as mock responses to send back thus removing the + * need for an actual server. + * + *

Below is an example that assumes static imports from + * {@code MockRestRequestMatchers}, {@code MockRestResponseCreators}, + * and {@code ExpectedCount}: * - *

Below is an example: *

- * import static org.springframework.test.web.client.match.MockRestRequestMatchers.method;
- * import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
- * import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
- *
- * ...
- *
  * RestTemplate restTemplate = new RestTemplate()
- * MockRestServiceServer mockServer = MockRestServiceServer.createServer(restTemplate);
+ * MockRestServiceServer server = MockRestServiceServer.restTemplate(restTemplate).build();
  *
- * mockServer.expect(requestTo("/hotels/42")).andExpect(method(HttpMethod.GET))
+ * server.expect(manyTimes(), requestTo("/hotels/42")).andExpect(method(HttpMethod.GET))
  *     .andRespond(withSuccess("{ \"id\" : \"42\", \"name\" : \"Holiday Inn\"}", MediaType.APPLICATION_JSON));
  *
  * Hotel hotel = restTemplate.getForObject("/hotels/{id}", Hotel.class, 42);
  * // Use the hotel instance...
  *
- * mockServer.verify();
+ * // Verify all expectations met
+ * server.verify();
  * 
* - *

To create an instance of this class, use {@link #createServer(RestTemplate)} - * and provide the {@code RestTemplate} to set up for the mock testing. - * - *

After that use {@link #expect(RequestMatcher)} and fluent API methods - * {@link ResponseActions#andExpect(RequestMatcher) andExpect(RequestMatcher)} and - * {@link ResponseActions#andRespond(ResponseCreator) andRespond(ResponseCreator)} - * to set up request expectations and responses, most likely relying on the default - * {@code RequestMatcher} implementations provided in {@link MockRestRequestMatchers} - * and the {@code ResponseCreator} implementations provided in - * {@link MockRestResponseCreators} both of which can be statically imported. - * - *

At the end of the test use {@link #verify()} to ensure all expected - * requests were actually performed. - * - *

Note that because of the fluent API offered by this class (and related - * classes), you can typically use the Code Completion features (i.e. - * ctrl-space) in your IDE to set up the mocks. - * - *

An alternative to the above is to use - * {@link MockMvcClientHttpRequestFactory} which allows executing requests - * against a {@link org.springframework.test.web.servlet.MockMvc MockMvc} - * instance. That allows you to process requests using your server-side code - * but without running a server. - * - *

Credits: The client-side REST testing support was - * inspired by and initially based on similar code in the Spring WS project for - * client-side tests involving the {@code WebServiceTemplate}. + *

Note that as an alternative to the above you can also set the + * {@link MockMvcClientHttpRequestFactory} on a {@code RestTemplate} which + * allows executing requests against an instance of + * {@link org.springframework.test.web.servlet.MockMvc MockMvc}. * * @author Craig Walls * @author Rossen Stoyanchev @@ -98,14 +72,6 @@ public class MockRestServiceServer { private final RequestExpectationManager expectationManager; - /** - * Private constructor. - * See static builder methods and {@code createServer} shortcut methods. - */ - private MockRestServiceServer() { - this.expectationManager = new SimpleRequestExpectationManager(); - } - /** * Private constructor with {@code RequestExpectationManager}. * See static builder methods and {@code createServer} shortcut methods. @@ -116,17 +82,37 @@ public class MockRestServiceServer { /** - * Set up a new HTTP request expectation. The returned {@link ResponseActions} - * is used to set up further expectations and to define the response. - *

This method may be invoked multiple times before starting the test, i.e. before - * using the {@code RestTemplate}, to set up expectations for multiple requests. - * @param matcher a request expectation, see {@link MockRestRequestMatchers} - * @return used to set up further expectations or to define a response + * Set up an expectation for a single HTTP request. The returned + * {@link ResponseActions} can be used to set up further expectations as + * well as to define the response. + * + *

This method may be invoked any number times before starting to make + * request through the underlying {@code RestTemplate} in order to set up + * all expected requests. + * + * @param matcher request matcher + * @return a representation of the expectation */ public ResponseActions expect(RequestMatcher matcher) { - return this.expectationManager.expectRequest(matcher); + return expect(ExpectedCount.once(), matcher); } + /** + * An alternative to {@link #expect(RequestMatcher)} with an indication how + * many times the request is expected to be executed. + * + *

When request expectations have an expected count greater than one, only + * the first execution is expected to match the order of declaration. Subsequent + * request executions may be inserted anywhere thereafter. + * + * @param count the expected count + * @param matcher request matcher + * @return a representation of the expectation + * @since 4.3 + */ + public ResponseActions expect(ExpectedCount count, RequestMatcher matcher) { + return this.expectationManager.expectRequest(count, matcher); + } /** * Verify that all expected requests set up via @@ -139,7 +125,7 @@ public class MockRestServiceServer { /** - * Build a {@code MockRestServiceServer} with a {@code RestTemplate}. + * Build a {@code MockRestServiceServer} for a {@code RestTemplate}. * @since 4.3 */ public static MockRestServiceServerBuilder restTemplate(RestTemplate restTemplate) { @@ -147,7 +133,7 @@ public class MockRestServiceServer { } /** - * Build a {@code MockRestServiceServer} with an {@code AsyncRestTemplate}. + * Build a {@code MockRestServiceServer} for an {@code AsyncRestTemplate}. * @since 4.3 */ public static MockRestServiceServerBuilder asyncRestTemplate(AsyncRestTemplate asyncRestTemplate) { @@ -155,7 +141,7 @@ public class MockRestServiceServer { } /** - * Build a {@code MockRestServiceServer} with a {@code RestGateway}. + * Build a {@code MockRestServiceServer} for a {@code RestGateway}. * @since 4.3 */ public static MockRestServiceServerBuilder restGateway(RestGatewaySupport restGateway) { @@ -195,7 +181,6 @@ public class MockRestServiceServer { /** * Builder to create a {@code MockRestServiceServer}. - */ public interface MockRestServiceServerBuilder { diff --git a/spring-test/src/main/java/org/springframework/test/web/client/RequestExpectation.java b/spring-test/src/main/java/org/springframework/test/web/client/RequestExpectation.java index 971139ac1a..b345c91bcb 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/RequestExpectation.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/RequestExpectation.java @@ -19,13 +19,23 @@ package org.springframework.test.web.client; * An extension of {@code ResponseActions} that also implements * {@code RequestMatcher} and {@code ResponseCreator} * - *

{@code ResponseActions} is the API for defining expectations while - * {@code RequestExpectation} is the internal SPI to match those expectations - * to actual requests and to create responses. + *

While {@code ResponseActions} is the API for defining expectations this + * sub-interface is the internal SPI for matching these expectations to actual + * requests and for creating responses. * * @author Rossen Stoyanchev * @since 4.3 */ public interface RequestExpectation extends ResponseActions, RequestMatcher, ResponseCreator { + /** + * Whether there is a remaining count of invocations for this expectation. + */ + boolean hasRemainingCount(); + + /** + * Whether the requirements for this request expectation have been met. + */ + boolean isSatisfied(); + } diff --git a/spring-test/src/main/java/org/springframework/test/web/client/RequestExpectationManager.java b/spring-test/src/main/java/org/springframework/test/web/client/RequestExpectationManager.java index d15b8094ee..112d5dd224 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/RequestExpectationManager.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/RequestExpectationManager.java @@ -21,9 +21,9 @@ import org.springframework.http.client.ClientHttpRequest; import org.springframework.http.client.ClientHttpResponse; /** - * Contract to manage creating HTTP request expectations, apply them to actual - * requests (in strict or random order), and at the end verify whether all - * expectations were met. + * Abstraction for creating HTTP request expectations, applying them to actual + * requests (in strict or random order), and verifying whether expectations + * have been met. * * @author Rossen Stoyanchev * @since 4.3 @@ -36,11 +36,11 @@ public interface RequestExpectationManager { * @param requestMatcher a request expectation * @return for setting up further expectations and define a response */ - ResponseActions expectRequest(RequestMatcher requestMatcher); + ResponseActions expectRequest(ExpectedCount count, RequestMatcher requestMatcher); /** - * Validate the given actual request against the declared expectations - * raising {@link AssertionError} if not met. + * Validate the given actual request against the declared expectations. + * Is successful return the mock response to use or raise an error. * @param request the request * @return the response to return if the request was validated. * @throws AssertionError when some expectations were not met diff --git a/spring-test/src/main/java/org/springframework/test/web/client/RequestMatcher.java b/spring-test/src/main/java/org/springframework/test/web/client/RequestMatcher.java index bc169f4881..14237729aa 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/RequestMatcher.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/RequestMatcher.java @@ -23,6 +23,9 @@ import org.springframework.http.client.ClientHttpRequest; /** * A contract for matching requests to expectations. * + *

See {@link org.springframework.test.web.client.match.MockRestRequestMatchers + * MockRestRequestMatchers} for static factory methods. + * * @author Craig Walls * @since 3.2 */ diff --git a/spring-test/src/main/java/org/springframework/test/web/client/SimpleRequestExpectationManager.java b/spring-test/src/main/java/org/springframework/test/web/client/SimpleRequestExpectationManager.java index ba37fbb18a..f07e0bc1c1 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/SimpleRequestExpectationManager.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/SimpleRequestExpectationManager.java @@ -16,64 +16,59 @@ package org.springframework.test.web.client; import java.io.IOException; -import java.net.URI; import java.util.Iterator; -import org.springframework.http.HttpMethod; import org.springframework.http.client.ClientHttpRequest; import org.springframework.http.client.ClientHttpResponse; +import org.springframework.util.Assert; /** * Simple {@code RequestExpectationManager} that matches requests to expectations * sequentially, i.e. in the order of declaration of expectations. * + *

When request expectations have an expected count greater than one, only + * the first execution is expected to match the order of declaration. Subsequent + * request executions may be inserted anywhere thereafter. + * * @author Rossen Stoyanchev * @since 4.3 */ public class SimpleRequestExpectationManager extends AbstractRequestExpectationManager { - private Iterator iterator; + private Iterator expectationIterator; + private final RequestExpectationGroup repeatExpectations = new RequestExpectationGroup(); + + + @Override + protected void afterExpectationsDeclared() { + Assert.state(this.expectationIterator == null); + this.expectationIterator = getExpectations().iterator(); + } @Override public ClientHttpResponse validateRequestInternal(ClientHttpRequest request) throws IOException { - if (this.iterator == null) { - this.iterator = getExpectations().iterator(); + RequestExpectation expectation; + try { + expectation = next(request); + expectation.match(request); } - if (!this.iterator.hasNext()) { - HttpMethod method = request.getMethod(); - URI uri = request.getURI(); - String firstLine = "No further requests expected: HTTP " + method + " " + uri + "\n"; - throw new AssertionError(createErrorMessage(firstLine)); - } - RequestExpectation expectation = this.iterator.next(); - expectation.match(request); - return expectation.createResponse(request); - } - - @Override - public void verify() { - if (getExpectations().isEmpty() || getExpectations().size() == getRequests().size()) { - return; - } - throw new AssertionError(createErrorMessage("Further request(s) expected\n")); - } - - private String createErrorMessage(String firstLine) { - StringBuilder sb = new StringBuilder(firstLine); - if (getRequests().size() > 0) { - sb.append("The following "); - } - sb.append(getRequests().size()).append(" out of "); - sb.append(getExpectations().size()).append(" were executed"); - - if (getRequests().size() > 0) { - sb.append(":\n"); - for (ClientHttpRequest request : getRequests()) { - sb.append(request.toString()).append("\n"); + catch (AssertionError error) { + expectation = this.repeatExpectations.findExpectation(request); + if (expectation == null) { + throw error; } } - return sb.toString(); + ClientHttpResponse response = expectation.createResponse(request); + this.repeatExpectations.update(expectation); + return response; + } + + private RequestExpectation next(ClientHttpRequest request) { + if (this.expectationIterator.hasNext()) { + return this.expectationIterator.next(); + } + throw createUnexpectedRequestError(request); } } diff --git a/spring-test/src/main/java/org/springframework/test/web/client/UnorderedRequestExpectationManager.java b/spring-test/src/main/java/org/springframework/test/web/client/UnorderedRequestExpectationManager.java index eab0d4d455..7384ff05c7 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/UnorderedRequestExpectationManager.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/UnorderedRequestExpectationManager.java @@ -16,74 +16,36 @@ package org.springframework.test.web.client; import java.io.IOException; -import java.net.URI; -import java.util.LinkedList; -import java.util.List; -import org.springframework.http.HttpMethod; import org.springframework.http.client.ClientHttpRequest; import org.springframework.http.client.ClientHttpResponse; /** * {@code RequestExpectationManager} that matches requests to expectations - * regardless of the order of declaration of expectations. + * regardless of the order of declaration of expectated requests. * * @author Rossen Stoyanchev * @since 4.3 */ public class UnorderedRequestExpectationManager extends AbstractRequestExpectationManager { - private final List remainingExpectations = new LinkedList(); + private final RequestExpectationGroup remainingExpectations = new RequestExpectationGroup(); - protected List getRemainingExpectations() { - return this.remainingExpectations; + @Override + protected void afterExpectationsDeclared() { + this.remainingExpectations.updateAll(getExpectations()); } - @Override public ClientHttpResponse validateRequestInternal(ClientHttpRequest request) throws IOException { - if (getRequests().isEmpty()) { - getRemainingExpectations().addAll(getExpectations()); + RequestExpectation expectation = this.remainingExpectations.findExpectation(request); + if (expectation != null) { + ClientHttpResponse response = expectation.createResponse(request); + this.remainingExpectations.update(expectation); + return response; } - for (RequestExpectation expectation : getExpectations()) { - try { - expectation.match(request); - getRemainingExpectations().remove(expectation); - return expectation.createResponse(request); - } - catch (AssertionError error) { - // Ignore - } - } - HttpMethod method = request.getMethod(); - URI uri = request.getURI(); - throw new AssertionError("Unexpected request: HTTP " + method + " " + uri); - } - - @Override - public void verify() { - if (getExpectations().isEmpty() || this.remainingExpectations.isEmpty()) { - return; - } - throw new AssertionError(getVerifyMessage()); - } - - private String getVerifyMessage() { - StringBuilder sb = new StringBuilder("Further request(s) expected\n"); - if (getRequests().size() > 0) { - sb.append("The following "); - } - sb.append(getRequests().size()).append(" were executed"); - sb.append(" leaving ").append(this.remainingExpectations.size()).append(" expectations."); - - if (getRequests().size() > 0) { - sb.append(":\n"); - for (ClientHttpRequest request : getRequests()) { - sb.append(request.toString()).append("\n"); - } - } - return sb.toString(); + throw createUnexpectedRequestError(request); } } diff --git a/spring-test/src/test/java/org/springframework/test/web/client/DefaultRequestExpectationTests.java b/spring-test/src/test/java/org/springframework/test/web/client/DefaultRequestExpectationTests.java new file mode 100644 index 0000000000..aea4f2d811 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/client/DefaultRequestExpectationTests.java @@ -0,0 +1,99 @@ +/* + * Copyright 2002-2016 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 + * + * http://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; + +import java.net.URI; +import java.net.URISyntaxException; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.http.HttpMethod; +import org.springframework.http.client.ClientHttpRequest; +import org.springframework.mock.http.client.MockAsyncClientHttpRequest; + +import static junit.framework.TestCase.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.springframework.http.HttpMethod.GET; +import static org.springframework.http.HttpMethod.POST; +import static org.springframework.test.web.client.ExpectedCount.once; +import static org.springframework.test.web.client.ExpectedCount.times; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.method; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; + +/** + * Unit tests for {@link DefaultRequestExpectation}. + * @author Rossen Stoyanchev + */ +public class DefaultRequestExpectationTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + + @Test + public void match() throws Exception { + RequestExpectation expectation = new DefaultRequestExpectation(once(), requestTo("/foo")); + expectation.match(createRequest(GET, "/foo")); + } + + @Test + public void matchWithFailedExpection() throws Exception { + RequestExpectation expectation = new DefaultRequestExpectation(once(), requestTo("/foo")); + expectation.andExpect(method(POST)); + + this.thrown.expectMessage("Unexpected HttpMethod expected: but was:"); + expectation.match(createRequest(GET, "/foo")); + } + + @Test + public void hasRemainingCount() throws Exception { + RequestExpectation expectation = new DefaultRequestExpectation(times(2), requestTo("/foo")); + expectation.andRespond(withSuccess()); + + expectation.createResponse(createRequest(GET, "/foo")); + assertTrue(expectation.hasRemainingCount()); + + expectation.createResponse(createRequest(GET, "/foo")); + assertFalse(expectation.hasRemainingCount()); + } + + @Test + public void isSatisfied() throws Exception { + RequestExpectation expectation = new DefaultRequestExpectation(times(2), requestTo("/foo")); + expectation.andRespond(withSuccess()); + + expectation.createResponse(createRequest(GET, "/foo")); + assertFalse(expectation.isSatisfied()); + + expectation.createResponse(createRequest(GET, "/foo")); + assertTrue(expectation.isSatisfied()); + } + + + + private ClientHttpRequest createRequest(HttpMethod method, String url) { + try { + return new MockAsyncClientHttpRequest(method, new URI(url)); + } + catch (URISyntaxException ex) { + throw new IllegalStateException(ex); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/web/client/SimpleRequestExpectationManagerTests.java b/spring-test/src/test/java/org/springframework/test/web/client/SimpleRequestExpectationManagerTests.java index 3c649e5b40..d6f2b9cbaa 100644 --- a/spring-test/src/test/java/org/springframework/test/web/client/SimpleRequestExpectationManagerTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/client/SimpleRequestExpectationManagerTests.java @@ -19,67 +19,149 @@ package org.springframework.test.web.client; import java.net.URI; import java.net.URISyntaxException; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; import org.springframework.http.HttpMethod; import org.springframework.http.client.ClientHttpRequest; import org.springframework.mock.http.client.MockAsyncClientHttpRequest; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.springframework.test.web.client.match.MockRestRequestMatchers.anything; +import static org.springframework.http.HttpMethod.GET; +import static org.springframework.http.HttpMethod.POST; +import static org.springframework.test.web.client.ExpectedCount.max; +import static org.springframework.test.web.client.ExpectedCount.min; +import static org.springframework.test.web.client.ExpectedCount.once; +import static org.springframework.test.web.client.ExpectedCount.times; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.method; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; /** - * Unit tests for {@link AbstractRequestExpectationManager}. + * Unit tests for {@link SimpleRequestExpectationManager}. * @author Rossen Stoyanchev */ public class SimpleRequestExpectationManagerTests { private SimpleRequestExpectationManager manager = new SimpleRequestExpectationManager(); + @Rule + public ExpectedException thrown = ExpectedException.none(); + @Test - public void validateWithUnexpectedRequest() throws Exception { + public void unexpectedRequest() throws Exception { try { - this.manager.validateRequest(request(HttpMethod.GET, "/foo")); + this.manager.validateRequest(createRequest(GET, "/foo")); } catch (AssertionError error) { assertEquals("No further requests expected: HTTP GET /foo\n" + - "0 out of 0 were executed", error.getMessage()); + "0 request(s) executed.\n", error.getMessage()); } } @Test - public void verify() throws Exception { - this.manager.expectRequest(anything()).andRespond(withSuccess()); - this.manager.expectRequest(anything()).andRespond(withSuccess()); - - this.manager.validateRequest(request(HttpMethod.GET, "/foo")); - this.manager.validateRequest(request(HttpMethod.POST, "/bar")); + public void zeroExpectedRequests() throws Exception { this.manager.verify(); } @Test - public void verifyWithZeroExpectations() throws Exception { + public void sequentialRequests() throws Exception { + this.manager.expectRequest(once(), requestTo("/foo")).andExpect(method(GET)).andRespond(withSuccess()); + this.manager.expectRequest(once(), requestTo("/bar")).andExpect(method(GET)).andRespond(withSuccess()); + + this.manager.validateRequest(createRequest(GET, "/foo")); + this.manager.validateRequest(createRequest(GET, "/bar")); this.manager.verify(); } @Test - public void verifyWithRemainingExpectations() throws Exception { - this.manager.expectRequest(anything()).andRespond(withSuccess()); - this.manager.expectRequest(anything()).andRespond(withSuccess()); + public void sequentialRequestsTooMany() throws Exception { + this.manager.expectRequest(max(1), requestTo("/foo")).andExpect(method(GET)).andRespond(withSuccess()); + this.manager.expectRequest(max(1), requestTo("/bar")).andExpect(method(GET)).andRespond(withSuccess()); - this.manager.validateRequest(request(HttpMethod.GET, "/foo")); - try { - this.manager.verify(); - } - catch (AssertionError error) { - assertTrue(error.getMessage(), error.getMessage().contains("1 out of 2 were executed")); - } + this.thrown.expectMessage("No further requests expected: HTTP GET /baz\n" + + "2 request(s) executed:\n" + + "GET /foo\n" + + "GET /bar\n"); + + this.manager.validateRequest(createRequest(GET, "/foo")); + this.manager.validateRequest(createRequest(GET, "/bar")); + this.manager.validateRequest(createRequest(GET, "/baz")); } - private ClientHttpRequest request(HttpMethod method, String url) { + @Test + public void sequentialRequestsTooFew() throws Exception { + this.manager.expectRequest(min(1), requestTo("/foo")).andExpect(method(GET)).andRespond(withSuccess()); + this.manager.expectRequest(min(1), requestTo("/bar")).andExpect(method(GET)).andRespond(withSuccess()); + + this.thrown.expectMessage("Further request(s) expected leaving 1 unsatisfied expectation(s).\n" + + "1 request(s) executed:\nGET /foo\n"); + + this.manager.validateRequest(createRequest(GET, "/foo")); + this.manager.verify(); + } + + @Test + public void repeatedRequests() throws Exception { + this.manager.expectRequest(times(2), requestTo("/foo")).andExpect(method(GET)).andRespond(withSuccess()); + this.manager.expectRequest(times(2), requestTo("/bar")).andExpect(method(GET)).andRespond(withSuccess()); + + this.manager.validateRequest(createRequest(GET, "/foo")); + this.manager.validateRequest(createRequest(GET, "/bar")); + this.manager.validateRequest(createRequest(GET, "/foo")); + this.manager.validateRequest(createRequest(GET, "/bar")); + this.manager.verify(); + } + + @Test + public void repeatedRequestsTooMany() throws Exception { + this.manager.expectRequest(max(2), requestTo("/foo")).andExpect(method(GET)).andRespond(withSuccess()); + this.manager.expectRequest(max(2), requestTo("/bar")).andExpect(method(GET)).andRespond(withSuccess()); + + this.thrown.expectMessage("No further requests expected: HTTP GET /foo\n" + + "4 request(s) executed:\n" + + "GET /foo\n" + + "GET /bar\n" + + "GET /foo\n" + + "GET /bar\n"); + + this.manager.validateRequest(createRequest(GET, "/foo")); + this.manager.validateRequest(createRequest(GET, "/bar")); + this.manager.validateRequest(createRequest(GET, "/foo")); + this.manager.validateRequest(createRequest(GET, "/bar")); + this.manager.validateRequest(createRequest(GET, "/foo")); + } + + @Test + public void repeatedRequestsTooFew() throws Exception { + this.manager.expectRequest(min(2), requestTo("/foo")).andExpect(method(GET)).andRespond(withSuccess()); + this.manager.expectRequest(min(2), requestTo("/bar")).andExpect(method(GET)).andRespond(withSuccess()); + + this.thrown.expectMessage("3 request(s) executed:\n" + + "GET /foo\n" + + "GET /bar\n" + + "GET /foo\n"); + + this.manager.validateRequest(createRequest(GET, "/foo")); + this.manager.validateRequest(createRequest(GET, "/bar")); + this.manager.validateRequest(createRequest(GET, "/foo")); + this.manager.verify(); + } + + @Test + public void repeatedRequestsNotInOrder() throws Exception { + this.manager.expectRequest(times(2), requestTo("/foo")).andExpect(method(GET)).andRespond(withSuccess()); + this.manager.expectRequest(times(2), requestTo("/bar")).andExpect(method(GET)).andRespond(withSuccess()); + this.manager.expectRequest(times(2), requestTo("/baz")).andExpect(method(GET)).andRespond(withSuccess()); + + this.thrown.expectMessage("Unexpected HttpMethod expected: but was:"); + this.manager.validateRequest(createRequest(POST, "/foo")); + } + + + private ClientHttpRequest createRequest(HttpMethod method, String url) { try { return new MockAsyncClientHttpRequest(method, new URI(url)); } diff --git a/spring-test/src/test/java/org/springframework/test/web/client/UnorderedRequestExpectationManagerTests.java b/spring-test/src/test/java/org/springframework/test/web/client/UnorderedRequestExpectationManagerTests.java new file mode 100644 index 0000000000..163f3865e6 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/client/UnorderedRequestExpectationManagerTests.java @@ -0,0 +1,133 @@ +/* + * Copyright 2002-2016 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 + * + * http://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; + +import java.net.URI; +import java.net.URISyntaxException; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.http.HttpMethod; +import org.springframework.http.client.ClientHttpRequest; +import org.springframework.mock.http.client.MockAsyncClientHttpRequest; + +import static org.junit.Assert.assertEquals; +import static org.springframework.http.HttpMethod.GET; +import static org.springframework.test.web.client.ExpectedCount.max; +import static org.springframework.test.web.client.ExpectedCount.min; +import static org.springframework.test.web.client.ExpectedCount.once; +import static org.springframework.test.web.client.ExpectedCount.times; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.method; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; + +/** + * Unit tests for {@link UnorderedRequestExpectationManager}. + * @author Rossen Stoyanchev + */ +public class UnorderedRequestExpectationManagerTests { + + private UnorderedRequestExpectationManager manager = new UnorderedRequestExpectationManager(); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + + @Test + public void unexpectedRequest() throws Exception { + try { + this.manager.validateRequest(createRequest(GET, "/foo")); + } + catch (AssertionError error) { + assertEquals("No further requests expected: HTTP GET /foo\n" + + "0 request(s) executed.\n", error.getMessage()); + } + } + + @Test + public void zeroExpectedRequests() throws Exception { + this.manager.verify(); + } + + @Test + public void multipleRequests() throws Exception { + this.manager.expectRequest(once(), requestTo("/foo")).andExpect(method(GET)).andRespond(withSuccess()); + this.manager.expectRequest(once(), requestTo("/bar")).andExpect(method(GET)).andRespond(withSuccess()); + + this.manager.validateRequest(createRequest(GET, "/bar")); + this.manager.validateRequest(createRequest(GET, "/foo")); + this.manager.verify(); + } + + @Test + public void repeatedRequests() throws Exception { + this.manager.expectRequest(times(2), requestTo("/foo")).andExpect(method(GET)).andRespond(withSuccess()); + this.manager.expectRequest(times(2), requestTo("/bar")).andExpect(method(GET)).andRespond(withSuccess()); + + this.manager.validateRequest(createRequest(GET, "/bar")); + this.manager.validateRequest(createRequest(GET, "/foo")); + this.manager.validateRequest(createRequest(GET, "/foo")); + this.manager.validateRequest(createRequest(GET, "/bar")); + this.manager.verify(); + } + + @Test + public void repeatedRequestsTooMany() throws Exception { + this.manager.expectRequest(max(2), requestTo("/foo")).andExpect(method(GET)).andRespond(withSuccess()); + this.manager.expectRequest(max(2), requestTo("/bar")).andExpect(method(GET)).andRespond(withSuccess()); + + this.thrown.expectMessage("No further requests expected: HTTP GET /foo\n" + + "4 request(s) executed:\n" + + "GET /bar\n" + + "GET /foo\n" + + "GET /bar\n" + + "GET /foo\n"); + + this.manager.validateRequest(createRequest(GET, "/bar")); + this.manager.validateRequest(createRequest(GET, "/foo")); + this.manager.validateRequest(createRequest(GET, "/bar")); + this.manager.validateRequest(createRequest(GET, "/foo")); + this.manager.validateRequest(createRequest(GET, "/foo")); + } + + @Test + public void repeatedRequestsTooFew() throws Exception { + this.manager.expectRequest(min(2), requestTo("/foo")).andExpect(method(GET)).andRespond(withSuccess()); + this.manager.expectRequest(min(2), requestTo("/bar")).andExpect(method(GET)).andRespond(withSuccess()); + + this.thrown.expectMessage("3 request(s) executed:\n" + + "GET /bar\n" + + "GET /foo\n" + + "GET /foo\n"); + + this.manager.validateRequest(createRequest(GET, "/bar")); + this.manager.validateRequest(createRequest(GET, "/foo")); + this.manager.validateRequest(createRequest(GET, "/foo")); + this.manager.verify(); + } + + + private ClientHttpRequest createRequest(HttpMethod method, String url) { + try { + return new MockAsyncClientHttpRequest(method, new URI(url)); + } + catch (URISyntaxException ex) { + throw new IllegalStateException(ex); + } + } +} diff --git a/spring-test/src/test/java/org/springframework/test/web/client/samples/SampleAsyncTests.java b/spring-test/src/test/java/org/springframework/test/web/client/samples/SampleAsyncTests.java index 73b17cb548..0f372039f0 100644 --- a/spring-test/src/test/java/org/springframework/test/web/client/samples/SampleAsyncTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/client/samples/SampleAsyncTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 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. @@ -24,11 +24,13 @@ import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.test.web.Person; +import org.springframework.test.web.client.ExpectedCount; import org.springframework.test.web.client.MockRestServiceServer; import org.springframework.util.concurrent.ListenableFuture; import org.springframework.web.client.AsyncRestTemplate; import static org.junit.Assert.*; +import static org.springframework.test.web.client.ExpectedCount.manyTimes; import static org.springframework.test.web.client.match.MockRestRequestMatchers.*; import static org.springframework.test.web.client.response.MockRestResponseCreators.*; @@ -63,7 +65,8 @@ public class SampleAsyncTests { .andRespond(withSuccess(responseBody, MediaType.APPLICATION_JSON)); @SuppressWarnings("unused") - ListenableFuture> ludwig = restTemplate.getForEntity("/composers/{id}", Person.class, 42); + ListenableFuture> ludwig = + this.restTemplate.getForEntity("/composers/{id}", Person.class, 42); // We are only validating the request. The response is mocked out. // person.getName().equals("Ludwig van Beethoven") @@ -73,19 +76,26 @@ public class SampleAsyncTests { } @Test - public void performGetAsync() throws Exception { + public void performGetManyTimes() throws Exception { String responseBody = "{\"name\" : \"Ludwig van Beethoven\", \"someDouble\" : \"1.6035\"}"; - this.mockServer.expect(requestTo("/composers/42")).andExpect(method(HttpMethod.GET)) + this.mockServer.expect(manyTimes(), requestTo("/composers/42")).andExpect(method(HttpMethod.GET)) .andRespond(withSuccess(responseBody, MediaType.APPLICATION_JSON)); @SuppressWarnings("unused") - ListenableFuture> ludwig = restTemplate.getForEntity("/composers/{id}", Person.class, 42); + ListenableFuture> ludwig = + this.restTemplate.getForEntity("/composers/{id}", Person.class, 42); + // We are only validating the request. The response is mocked out. // person.getName().equals("Ludwig van Beethoven") // person.getDouble().equals(1.6035) + this.restTemplate.getForEntity("/composers/{id}", Person.class, 42); + this.restTemplate.getForEntity("/composers/{id}", Person.class, 42); + this.restTemplate.getForEntity("/composers/{id}", Person.class, 42); + this.restTemplate.getForEntity("/composers/{id}", Person.class, 42); + this.mockServer.verify(); } @@ -98,7 +108,8 @@ public class SampleAsyncTests { .andRespond(withSuccess(responseBody, MediaType.APPLICATION_JSON)); @SuppressWarnings("unused") - ListenableFuture> ludwig = restTemplate.getForEntity("/composers/{id}", Person.class, 42); + ListenableFuture> ludwig = + this.restTemplate.getForEntity("/composers/{id}", Person.class, 42); // hotel.getId() == 42 // hotel.getName().equals("Holiday Inn") @@ -132,7 +143,7 @@ public class SampleAsyncTests { this.mockServer.verify(); } catch (AssertionError error) { - assertTrue(error.getMessage(), error.getMessage().contains("2 out of 4 were executed")); + assertTrue(error.getMessage(), error.getMessage().contains("2 unsatisfied expectation(s)")); } } } diff --git a/spring-test/src/test/java/org/springframework/test/web/client/samples/SampleTests.java b/spring-test/src/test/java/org/springframework/test/web/client/samples/SampleTests.java index d3fc4823e4..cb882f7c1b 100644 --- a/spring-test/src/test/java/org/springframework/test/web/client/samples/SampleTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/client/samples/SampleTests.java @@ -23,10 +23,12 @@ import org.springframework.core.io.Resource; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.test.web.Person; +import org.springframework.test.web.client.ExpectedCount; import org.springframework.test.web.client.MockRestServiceServer; import org.springframework.web.client.RestTemplate; import static org.junit.Assert.assertTrue; +import static org.springframework.test.web.client.ExpectedCount.manyTimes; import static org.springframework.test.web.client.match.MockRestRequestMatchers.method; import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; @@ -60,7 +62,7 @@ public class SampleTests { .andRespond(withSuccess(responseBody, MediaType.APPLICATION_JSON)); @SuppressWarnings("unused") - Person ludwig = restTemplate.getForObject("/composers/{id}", Person.class, 42); + Person ludwig = this.restTemplate.getForObject("/composers/{id}", Person.class, 42); // We are only validating the request. The response is mocked out. // hotel.getId() == 42 @@ -69,6 +71,28 @@ public class SampleTests { this.mockServer.verify(); } + @Test + public void performGetManyTimes() throws Exception { + + String responseBody = "{\"name\" : \"Ludwig van Beethoven\", \"someDouble\" : \"1.6035\"}"; + + this.mockServer.expect(manyTimes(), requestTo("/composers/42")).andExpect(method(HttpMethod.GET)) + .andRespond(withSuccess(responseBody, MediaType.APPLICATION_JSON)); + + @SuppressWarnings("unused") + Person ludwig = this.restTemplate.getForObject("/composers/{id}", Person.class, 42); + + // We are only validating the request. The response is mocked out. + // hotel.getId() == 42 + // hotel.getName().equals("Holiday Inn") + + this.restTemplate.getForObject("/composers/{id}", Person.class, 42); + this.restTemplate.getForObject("/composers/{id}", Person.class, 42); + this.restTemplate.getForObject("/composers/{id}", Person.class, 42); + + this.mockServer.verify(); + } + @Test public void performGetWithResponseBodyFromFile() throws Exception { @@ -78,7 +102,7 @@ public class SampleTests { .andRespond(withSuccess(responseBody, MediaType.APPLICATION_JSON)); @SuppressWarnings("unused") - Person ludwig = restTemplate.getForObject("/composers/{id}", Person.class, 42); + Person ludwig = this.restTemplate.getForObject("/composers/{id}", Person.class, 42); // hotel.getId() == 42 // hotel.getName().equals("Holiday Inn") @@ -112,7 +136,7 @@ public class SampleTests { this.mockServer.verify(); } catch (AssertionError error) { - assertTrue(error.getMessage(), error.getMessage().contains("2 out of 4 were executed")); + assertTrue(error.getMessage(), error.getMessage().contains("2 unsatisfied expectation(s)")); } } } diff --git a/src/asciidoc/testing.adoc b/src/asciidoc/testing.adoc index 648ae23923..d789e3491b 100644 --- a/src/asciidoc/testing.adoc +++ b/src/asciidoc/testing.adoc @@ -5045,8 +5045,9 @@ Here is an example: ---- RestTemplate restTemplate = new RestTemplate(); - MockRestServiceServer mockServer = MockRestServiceServer.createServer(restTemplate); - mockServer.expect(requestTo("/greeting")).andRespond(withSuccess("Hello world", MediaType.TEXT_PLAIN)); + MockRestServiceServer mockServer = MockRestServiceServer.restTemplate(restTemplate).build(); + mockServer.expect(manyTimes(), requestTo("/greeting")) + .andRespond(withSuccess("Hello world", MediaType.TEXT_PLAIN)); // Test code that uses the above RestTemplate ... diff --git a/src/asciidoc/whats-new.adoc b/src/asciidoc/whats-new.adoc index c1971e7fd7..006c40722d 100644 --- a/src/asciidoc/whats-new.adoc +++ b/src/asciidoc/whats-new.adoc @@ -678,4 +678,5 @@ Spring 4.3 also improves the caching abstraction as follows: * The JUnit support in the _Spring TestContext Framework_ now requires JUnit 4.12 or higher. * Server-side Spring MVC Test supports expectations on response headers with multiple values. * Server-side Spring MVC Test parses form data request content and populates request parameters. +* Client-side Spring MVC Test supports expected count of request executions (once, manyTimes, min, max, etc.) * Client-side Spring MVC Test supports expectations for form data in the request body.