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
new file mode 100644
index 0000000000..3591784ce4
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/web/client/AbstractRequestExpectationManager.java
@@ -0,0 +1,180 @@
+/*
+ * 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.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 actual requests, and checking for unsatisfied
+ * 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
+ */
+public abstract class AbstractRequestExpectationManager implements RequestExpectationManager {
+
+ private final List expectations = new LinkedList();
+
+ private final List requests = new LinkedList();
+
+
+ protected List getExpectations() {
+ return this.expectations;
+ }
+
+ protected List getRequests() {
+ return this.requests;
+ }
+
+
+ @Override
+ public ResponseActions expectRequest(ExpectedCount count, RequestMatcher matcher) {
+ Assert.state(getRequests().isEmpty(), "Cannot add more expectations after actual requests are made.");
+ RequestExpectation expectation = new DefaultRequestExpectation(count, matcher);
+ getExpectations().add(expectation);
+ return expectation;
+ }
+
+ @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
new file mode 100644
index 0000000000..7c4950dbee
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/web/client/DefaultRequestExpectation.java
@@ -0,0 +1,147 @@
+/*
+ * 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.io.IOException;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.springframework.http.client.ClientHttpRequest;
+import org.springframework.http.client.ClientHttpResponse;
+import org.springframework.util.Assert;
+
+/**
+ * Default implementation of {@code RequestExpectation} that simply delegates
+ * to the request matchers and the response creator it contains.
+ *
+ * @author Rossen Stoyanchev
+ * @since 4.3
+ */
+public class DefaultRequestExpectation implements RequestExpectation {
+
+ private final RequestCount requestCount;
+
+ private final List requestMatchers = new LinkedList();
+
+ private ResponseCreator responseCreator;
+
+
+ /**
+ * 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");
+ this.requestMatchers.add(requestMatcher);
+ return this;
+ }
+
+ @Override
+ public void andRespond(ResponseCreator responseCreator) {
+ Assert.notNull(responseCreator, "ResponseCreator is required");
+ this.responseCreator = responseCreator;
+ }
+
+ @Override
+ public void match(ClientHttpRequest request) throws IOException {
+ for (RequestMatcher matcher : getRequestMatchers()) {
+ matcher.match(request);
+ }
+ }
+
+ @Override
+ public ClientHttpResponse createResponse(ClientHttpRequest request) throws IOException {
+ if (getResponseCreator() == null) {
+ throw new IllegalStateException("createResponse called before ResponseCreator was set.");
+ }
+ 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 582a55ca94..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
@@ -18,15 +18,14 @@ package org.springframework.test.web.client;
import java.io.IOException;
import java.net.URI;
-import java.util.Iterator;
-import java.util.LinkedList;
-import java.util.List;
import org.springframework.http.HttpMethod;
import org.springframework.http.client.AsyncClientHttpRequest;
import org.springframework.http.client.AsyncClientHttpRequestFactory;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpRequestFactory;
+import org.springframework.http.client.ClientHttpResponse;
+import org.springframework.mock.http.client.MockAsyncClientHttpRequest;
import org.springframework.test.web.client.match.MockRestRequestMatchers;
import org.springframework.test.web.client.response.MockRestResponseCreators;
import org.springframework.util.Assert;
@@ -36,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
@@ -96,75 +69,49 @@ import org.springframework.web.client.support.RestGatewaySupport;
*/
public class MockRestServiceServer {
- private final List expectedRequests =
- new LinkedList();
-
- private final List actualRequests =
- new LinkedList();
+ private final RequestExpectationManager expectationManager;
/**
- * Private constructor.
- * @see #createServer(RestTemplate)
- * @see #createServer(RestGatewaySupport)
+ * Private constructor with {@code RequestExpectationManager}.
+ * See static builder methods and {@code createServer} shortcut methods.
*/
- private MockRestServiceServer() {
+ private MockRestServiceServer(RequestExpectationManager expectationManager) {
+ this.expectationManager = expectationManager;
}
/**
- * Create a {@code MockRestServiceServer} and set up the given
- * {@code RestTemplate} with a mock {@link ClientHttpRequestFactory}.
- * @param restTemplate the RestTemplate to set up for mock testing
- * @return the created mock server
+ * 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 static MockRestServiceServer createServer(RestTemplate restTemplate) {
- Assert.notNull(restTemplate, "'restTemplate' must not be null");
- MockRestServiceServer mockServer = new MockRestServiceServer();
- RequestMatcherClientHttpRequestFactory factory = mockServer.new RequestMatcherClientHttpRequestFactory();
- restTemplate.setRequestFactory(factory);
- return mockServer;
+ public ResponseActions expect(RequestMatcher matcher) {
+ return expect(ExpectedCount.once(), matcher);
}
/**
- * Create a {@code MockRestServiceServer} and set up the given
- * {@code AsyRestTemplate} with a mock {@link AsyncClientHttpRequestFactory}.
- * @param asyncRestTemplate the AsyncRestTemplate to set up for mock testing
- * @return the created mock server
+ * 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 static MockRestServiceServer createServer(AsyncRestTemplate asyncRestTemplate) {
- Assert.notNull(asyncRestTemplate, "'asyncRestTemplate' must not be null");
- MockRestServiceServer mockServer = new MockRestServiceServer();
- RequestMatcherClientHttpRequestFactory factory = mockServer.new RequestMatcherClientHttpRequestFactory();
- asyncRestTemplate.setAsyncRequestFactory(factory);
- return mockServer;
- }
-
- /**
- * Create a {@code MockRestServiceServer} and set up the given
- * {@code RestGatewaySupport} with a mock {@link ClientHttpRequestFactory}.
- * @param restGateway the REST gateway to set up for mock testing
- * @return the created mock server
- */
- public static MockRestServiceServer createServer(RestGatewaySupport restGateway) {
- Assert.notNull(restGateway, "'gatewaySupport' must not be null");
- return createServer(restGateway.getRestTemplate());
- }
-
-
- /**
- * 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 requestMatcher a request expectation, see {@link MockRestRequestMatchers}
- * @return used to set up further expectations or to define a response
- */
- public ResponseActions expect(RequestMatcher requestMatcher) {
- Assert.state(this.actualRequests.isEmpty(), "Can't add more expected requests with test already underway");
- RequestMatcherClientHttpRequest request = new RequestMatcherClientHttpRequest(requestMatcher);
- this.expectedRequests.add(request);
- return request;
+ public ResponseActions expect(ExpectedCount count, RequestMatcher matcher) {
+ return this.expectationManager.expectRequest(count, matcher);
}
/**
@@ -173,66 +120,173 @@ public class MockRestServiceServer {
* @throws AssertionError when some expectations were not met
*/
public void verify() {
- if (this.expectedRequests.isEmpty() || this.expectedRequests.equals(this.actualRequests)) {
- return;
- }
- throw new AssertionError(getVerifyMessage());
+ this.expectationManager.verify();
}
- private String getVerifyMessage() {
- StringBuilder sb = new StringBuilder("Further request(s) expected\n");
- if (this.actualRequests.size() > 0) {
- sb.append("The following ");
- }
- sb.append(this.actualRequests.size()).append(" out of ");
- sb.append(this.expectedRequests.size()).append(" were executed");
- if (this.actualRequests.size() > 0) {
- sb.append(":\n");
- for (RequestMatcherClientHttpRequest request : this.actualRequests) {
- sb.append(request.toString()).append("\n");
- }
+ /**
+ * Build a {@code MockRestServiceServer} for a {@code RestTemplate}.
+ * @since 4.3
+ */
+ public static MockRestServiceServerBuilder restTemplate(RestTemplate restTemplate) {
+ return new DefaultBuilder(restTemplate);
+ }
+
+ /**
+ * Build a {@code MockRestServiceServer} for an {@code AsyncRestTemplate}.
+ * @since 4.3
+ */
+ public static MockRestServiceServerBuilder asyncRestTemplate(AsyncRestTemplate asyncRestTemplate) {
+ return new DefaultBuilder(asyncRestTemplate);
+ }
+
+ /**
+ * Build a {@code MockRestServiceServer} for a {@code RestGateway}.
+ * @since 4.3
+ */
+ public static MockRestServiceServerBuilder restGateway(RestGatewaySupport restGateway) {
+ Assert.notNull(restGateway, "'gatewaySupport' must not be null");
+ return new DefaultBuilder(restGateway.getRestTemplate());
+ }
+
+
+ /**
+ * A shortcut for {@code restTemplate(restTemplate).build()}.
+ * @param restTemplate the RestTemplate to set up for mock testing
+ * @return the mock server
+ */
+ public static MockRestServiceServer createServer(RestTemplate restTemplate) {
+ return restTemplate(restTemplate).build();
+ }
+
+ /**
+ * A shortcut for {@code asyncRestTemplate(asyncRestTemplate).build()}.
+ * @param asyncRestTemplate the AsyncRestTemplate to set up for mock testing
+ * @return the created mock server
+ */
+ public static MockRestServiceServer createServer(AsyncRestTemplate asyncRestTemplate) {
+ return asyncRestTemplate(asyncRestTemplate).build();
+ }
+
+ /**
+ * A shortcut for {@code restGateway(restGateway).build()}.
+ * @param restGateway the REST gateway to set up for mock testing
+ * @return the created mock server
+ */
+ public static MockRestServiceServer createServer(RestGatewaySupport restGateway) {
+ return restGateway(restGateway).build();
+ }
+
+
+
+ /**
+ * Builder to create a {@code MockRestServiceServer}.
+ */
+ public interface MockRestServiceServerBuilder {
+
+ /**
+ * Allow expected requests to be executed in any order not necessarily
+ * matching the order of declaration. This is a shortcut for:
+ * {@code builder.expectationManager(new UnorderedRequestExpectationManager)}
+ */
+ MockRestServiceServerBuilder unordered();
+
+ /**
+ * Configure a custom {@code RequestExpectationManager}.
+ *
By default {@link SimpleRequestExpectationManager} is used. It is
+ * also possible to switch to {@link UnorderedRequestExpectationManager}
+ * by setting {@link #unordered()}.
+ */
+ MockRestServiceServerBuilder expectationManager(RequestExpectationManager manager);
+
+ /**
+ * Build the {@code MockRestServiceServer} and setting up the underlying
+ * {@code RestTemplate} or {@code AsyncRestTemplate} with a
+ * {@link ClientHttpRequestFactory} that creates mock requests.
+ */
+ MockRestServiceServer build();
+
+ }
+
+ private static class DefaultBuilder implements MockRestServiceServerBuilder {
+
+ private final RestTemplate restTemplate;
+
+ private final AsyncRestTemplate asyncRestTemplate;
+
+ private RequestExpectationManager expectationManager = new SimpleRequestExpectationManager();
+
+
+ public DefaultBuilder(RestTemplate restTemplate) {
+ Assert.notNull(restTemplate, "'restTemplate' must not be null");
+ this.restTemplate = restTemplate;
+ this.asyncRestTemplate = null;
+ }
+
+ public DefaultBuilder(AsyncRestTemplate asyncRestTemplate) {
+ Assert.notNull(asyncRestTemplate, "'asyncRestTemplate' must not be null");
+ this.restTemplate = null;
+ this.asyncRestTemplate = asyncRestTemplate;
+ }
+
+
+ @Override
+ public MockRestServiceServerBuilder unordered() {
+ expectationManager(new UnorderedRequestExpectationManager());
+ return this;
+ }
+
+ @Override
+ public MockRestServiceServerBuilder expectationManager(RequestExpectationManager manager) {
+ Assert.notNull(manager, "'manager' is required.");
+ this.expectationManager = manager;
+ return this;
+ }
+
+ @Override
+ public MockRestServiceServer build() {
+ MockRestServiceServer server = new MockRestServiceServer(this.expectationManager);
+ MockClientHttpRequestFactory factory = server.new MockClientHttpRequestFactory();
+ if (this.restTemplate != null) {
+ this.restTemplate.setRequestFactory(factory);
+ }
+ if (this.asyncRestTemplate != null) {
+ this.asyncRestTemplate.setAsyncRequestFactory(factory);
+ }
+ return server;
}
- return sb.toString();
}
/**
* Mock ClientHttpRequestFactory that creates requests by iterating
- * over the list of expected {@link RequestMatcherClientHttpRequest}'s.
+ * over the list of expected {@link DefaultRequestExpectation}'s.
*/
- private class RequestMatcherClientHttpRequestFactory
- implements ClientHttpRequestFactory, AsyncClientHttpRequestFactory {
-
- private Iterator requestIterator;
+ private class MockClientHttpRequestFactory implements ClientHttpRequestFactory, AsyncClientHttpRequestFactory {
@Override
- public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
+ public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) {
return createRequestInternal(uri, httpMethod);
}
@Override
- public AsyncClientHttpRequest createAsyncRequest(URI uri, HttpMethod httpMethod) throws IOException {
+ public AsyncClientHttpRequest createAsyncRequest(URI uri, HttpMethod httpMethod) {
return createRequestInternal(uri, httpMethod);
}
- private RequestMatcherClientHttpRequest createRequestInternal(URI uri, HttpMethod httpMethod) {
+ private MockAsyncClientHttpRequest createRequestInternal(URI uri, HttpMethod method) {
Assert.notNull(uri, "'uri' must not be null");
- Assert.notNull(httpMethod, "'httpMethod' must not be null");
+ Assert.notNull(method, "'httpMethod' must not be null");
- if (this.requestIterator == null) {
- this.requestIterator = MockRestServiceServer.this.expectedRequests.iterator();
- }
- if (!this.requestIterator.hasNext()) {
- throw new AssertionError("No further requests expected: HTTP " + httpMethod + " " + uri);
- }
+ return new MockAsyncClientHttpRequest(method, uri) {
- RequestMatcherClientHttpRequest request = this.requestIterator.next();
- request.setURI(uri);
- request.setMethod(httpMethod);
-
- MockRestServiceServer.this.actualRequests.add(request);
- return request;
+ @Override
+ protected ClientHttpResponse executeInternal() throws IOException {
+ ClientHttpResponse response = expectationManager.validateRequest(this);
+ setResponse(response);
+ return response;
+ }
+ };
}
}
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
new file mode 100644
index 0000000000..b345c91bcb
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/web/client/RequestExpectation.java
@@ -0,0 +1,41 @@
+/*
+ * 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;
+
+/**
+ * An extension of {@code ResponseActions} that also implements
+ * {@code RequestMatcher} and {@code ResponseCreator}
+ *
+ * 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
new file mode 100644
index 0000000000..112d5dd224
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/web/client/RequestExpectationManager.java
@@ -0,0 +1,57 @@
+/*
+ * 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.io.IOException;
+
+import org.springframework.http.client.ClientHttpRequest;
+import org.springframework.http.client.ClientHttpResponse;
+
+/**
+ * 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
+ */
+public interface RequestExpectationManager {
+
+ /**
+ * Set up a new request expectation. The returned {@link ResponseActions} is
+ * used to add more expectations and define a response.
+ * @param requestMatcher a request expectation
+ * @return for setting up further expectations and define a response
+ */
+ ResponseActions expectRequest(ExpectedCount count, RequestMatcher requestMatcher);
+
+ /**
+ * 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
+ * @throws IOException
+ */
+ ClientHttpResponse validateRequest(ClientHttpRequest request) throws IOException;
+
+ /**
+ * Verify that all expectations have been met.
+ * @throws AssertionError when some expectations were not met
+ */
+ void verify();
+
+}
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/RequestMatcherClientHttpRequest.java b/spring-test/src/main/java/org/springframework/test/web/client/RequestMatcherClientHttpRequest.java
deleted file mode 100644
index 1a01328bdd..0000000000
--- a/spring-test/src/main/java/org/springframework/test/web/client/RequestMatcherClientHttpRequest.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Copyright 2002-2015 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.io.IOException;
-import java.util.LinkedList;
-import java.util.List;
-
-import org.springframework.http.client.ClientHttpResponse;
-import org.springframework.mock.http.client.MockAsyncClientHttpRequest;
-import org.springframework.util.Assert;
-
-/**
- * A specialization of {@code MockClientHttpRequest} that matches the request
- * against a set of expectations, via {@link RequestMatcher} instances. The
- * expectations are checked when the request is executed. This class also uses a
- * {@link ResponseCreator} to create the response.
- *
- * @author Craig Walls
- * @author Rossen Stoyanchev
- * @since 3.2
- */
-class RequestMatcherClientHttpRequest extends MockAsyncClientHttpRequest implements ResponseActions {
-
- private final List requestMatchers = new LinkedList();
-
- private ResponseCreator responseCreator;
-
-
- public RequestMatcherClientHttpRequest(RequestMatcher requestMatcher) {
- Assert.notNull(requestMatcher, "RequestMatcher is required");
- this.requestMatchers.add(requestMatcher);
- }
-
-
- @Override
- public ResponseActions andExpect(RequestMatcher requestMatcher) {
- Assert.notNull(requestMatcher, "RequestMatcher is required");
- this.requestMatchers.add(requestMatcher);
- return this;
- }
-
- @Override
- public void andRespond(ResponseCreator responseCreator) {
- Assert.notNull(responseCreator, "ResponseCreator is required");
- this.responseCreator = responseCreator;
- }
-
- @Override
- public ClientHttpResponse executeInternal() throws IOException {
- if (this.requestMatchers.isEmpty()) {
- throw new AssertionError("No request expectations to execute");
- }
-
- if (this.responseCreator == null) {
- throw new AssertionError("No ResponseCreator was set up. Add it after request expectations, " +
- "e.g. MockRestServiceServer.expect(requestTo(\"/foo\")).andRespond(withSuccess())");
- }
-
- for (RequestMatcher requestMatcher : this.requestMatchers) {
- requestMatcher.match(this);
- }
- setResponse(this.responseCreator.createResponse(this));
- return super.executeInternal();
- }
-
-}
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
new file mode 100644
index 0000000000..f07e0bc1c1
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/web/client/SimpleRequestExpectationManager.java
@@ -0,0 +1,74 @@
+/*
+ * 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.io.IOException;
+import java.util.Iterator;
+
+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 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 {
+ RequestExpectation expectation;
+ try {
+ expectation = next(request);
+ expectation.match(request);
+ }
+ catch (AssertionError error) {
+ expectation = this.repeatExpectations.findExpectation(request);
+ if (expectation == null) {
+ throw error;
+ }
+ }
+ 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
new file mode 100644
index 0000000000..7384ff05c7
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/web/client/UnorderedRequestExpectationManager.java
@@ -0,0 +1,51 @@
+/*
+ * 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.io.IOException;
+
+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 expectated requests.
+ *
+ * @author Rossen Stoyanchev
+ * @since 4.3
+ */
+public class UnorderedRequestExpectationManager extends AbstractRequestExpectationManager {
+
+ private final RequestExpectationGroup remainingExpectations = new RequestExpectationGroup();
+
+
+ @Override
+ protected void afterExpectationsDeclared() {
+ this.remainingExpectations.updateAll(getExpectations());
+ }
+
+ @Override
+ public ClientHttpResponse validateRequestInternal(ClientHttpRequest request) throws IOException {
+ RequestExpectation expectation = this.remainingExpectations.findExpectation(request);
+ if (expectation != null) {
+ ClientHttpResponse response = expectation.createResponse(request);
+ this.remainingExpectations.update(expectation);
+ return response;
+ }
+ 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/MockClientHttpRequestFactoryTests.java b/spring-test/src/test/java/org/springframework/test/web/client/MockClientHttpRequestFactoryTests.java
deleted file mode 100644
index dfd3a7147e..0000000000
--- a/spring-test/src/test/java/org/springframework/test/web/client/MockClientHttpRequestFactoryTests.java
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- * Copyright 2002-2014 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 org.junit.Before;
-import org.junit.Test;
-
-import org.springframework.http.HttpMethod;
-import org.springframework.http.client.ClientHttpRequest;
-import org.springframework.http.client.ClientHttpRequestFactory;
-import org.springframework.web.client.RestTemplate;
-
-import static org.junit.Assert.*;
-import static org.springframework.test.web.client.match.MockRestRequestMatchers.*;
-
-/**
- * Tests for
- * {@link org.springframework.test.web.client.MockMvcClientHttpRequestFactory}.
- *
- * @author Rossen Stoyanchev
- */
-public class MockClientHttpRequestFactoryTests {
-
- private MockRestServiceServer server;
-
- private ClientHttpRequestFactory factory;
-
-
- @Before
- public void setup() {
- RestTemplate restTemplate = new RestTemplate();
- this.server = MockRestServiceServer.createServer(restTemplate);
- this.factory = restTemplate.getRequestFactory();
- }
-
- @Test
- public void createRequest() throws Exception {
- URI uri = new URI("/foo");
- ClientHttpRequest expected = (ClientHttpRequest) this.server.expect(anything());
- ClientHttpRequest actual = this.factory.createRequest(uri, HttpMethod.GET);
-
- assertSame(expected, actual);
- assertEquals(uri, actual.getURI());
- assertEquals(HttpMethod.GET, actual.getMethod());
- }
-
- @Test
- public void noFurtherRequestsExpected() throws Exception {
- try {
- this.factory.createRequest(new URI("/foo"), HttpMethod.GET);
- }
- catch (AssertionError error) {
- assertEquals("No further requests expected: HTTP GET /foo", error.getMessage());
- }
- }
-
- @Test
- public void verifyZeroExpected() throws Exception {
- this.server.verify();
- }
-
- @Test
- public void verifyExpectedEqualExecuted() throws Exception {
- this.server.expect(anything());
- this.server.expect(anything());
-
- this.factory.createRequest(new URI("/foo"), HttpMethod.GET);
- this.factory.createRequest(new URI("/bar"), HttpMethod.POST);
- }
-
- @Test
- public void verifyMoreExpected() throws Exception {
- this.server.expect(anything());
- this.server.expect(anything());
-
- this.factory.createRequest(new URI("/foo"), HttpMethod.GET);
-
- try {
- this.server.verify();
- }
- catch (AssertionError error) {
- assertTrue(error.getMessage(), error.getMessage().contains("1 out of 2 were executed"));
- }
- }
-
-}
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
new file mode 100644
index 0000000000..d6f2b9cbaa
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/web/client/SimpleRequestExpectationManagerTests.java
@@ -0,0 +1,173 @@
+/*
+ * 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.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 SimpleRequestExpectationManager}.
+ * @author Rossen Stoyanchev
+ */
+public class SimpleRequestExpectationManagerTests {
+
+ private SimpleRequestExpectationManager manager = new SimpleRequestExpectationManager();
+
+ @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 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 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.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"));
+ }
+
+ @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));
+ }
+ catch (URISyntaxException ex) {
+ throw new IllegalStateException(ex);
+ }
+ }
+
+}
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.