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.