Merge client REST test support updates

Includes support to declare expecations should occur multiple
times as well as in any order.

Issue: SPR-11365
This commit is contained in:
Rossen Stoyanchev 2016-02-23 23:33:59 -05:00
commit 46189b48fc
18 changed files with 1317 additions and 334 deletions

View File

@ -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.
*
* <p>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<RequestExpectation> expectations = new LinkedList<RequestExpectation>();
private final List<ClientHttpRequest> requests = new LinkedList<ClientHttpRequest>();
protected List<RequestExpectation> getExpectations() {
return this.expectations;
}
protected List<ClientHttpRequest> 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<RequestExpectation> expectations = new LinkedHashSet<RequestExpectation>();
public Set<RequestExpectation> getExpectations() {
return this.expectations;
}
public void update(RequestExpectation expectation) {
if (expectation.hasRemainingCount()) {
getExpectations().add(expectation);
}
else {
getExpectations().remove(expectation);
}
}
public void updateAll(Collection<RequestExpectation> 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;
}
}
}

View File

@ -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<RequestMatcher> requestMatchers = new LinkedList<RequestMatcher>();
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<RequestMatcher> 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());
}
}
}

View File

@ -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.
*
* <p>Examples:
* <pre>
* import static org.springframework.test.web.client.ExpectedCount.*
*
* once()
* manyTimes()
* times(5)
* min(2)
* max(4)
* between(2, 4)
* </pre>
*
* @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);
}
}

View File

@ -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;
/**
* <strong>Main entry point for client-side REST testing</strong>. 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.
*
* <p>Below is an example that assumes static imports from
* {@code MockRestRequestMatchers}, {@code MockRestResponseCreators},
* and {@code ExpectedCount}:
*
* <p>Below is an example:
* <pre class="code">
* 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);
* &#47;&#47; Use the hotel instance...
*
* mockServer.verify();
* // Verify all expectations met
* server.verify();
* </pre>
*
* <p>To create an instance of this class, use {@link #createServer(RestTemplate)}
* and provide the {@code RestTemplate} to set up for the mock testing.
*
* <p>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.
*
* <p>At the end of the test use {@link #verify()} to ensure all expected
* requests were actually performed.
*
* <p>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.
*
* <p>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.
*
* <p><strong>Credits:</strong> 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}.
* <p>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<RequestMatcherClientHttpRequest> expectedRequests =
new LinkedList<RequestMatcherClientHttpRequest>();
private final List<RequestMatcherClientHttpRequest> actualRequests =
new LinkedList<RequestMatcherClientHttpRequest>();
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.
*
* <p>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.
*
* <p>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.
* <p>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:<br>
* {@code builder.expectationManager(new UnorderedRequestExpectationManager)}
*/
MockRestServiceServerBuilder unordered();
/**
* Configure a custom {@code RequestExpectationManager}.
* <p>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<RequestMatcherClientHttpRequest> 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;
}
};
}
}

View File

@ -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}
*
* <p>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();
}

View File

@ -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();
}

View File

@ -23,6 +23,9 @@ import org.springframework.http.client.ClientHttpRequest;
/**
* A contract for matching requests to expectations.
*
* <p>See {@link org.springframework.test.web.client.match.MockRestRequestMatchers
* MockRestRequestMatchers} for static factory methods.
*
* @author Craig Walls
* @since 3.2
*/

View File

@ -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<RequestMatcher> requestMatchers = new LinkedList<RequestMatcher>();
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();
}
}

View File

@ -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.
*
* <p>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<RequestExpectation> 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);
}
}

View File

@ -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);
}
}

View File

@ -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:<POST> but was:<GET>");
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);
}
}
}

View File

@ -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"));
}
}
}

View File

@ -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:<GET> but was:<POST>");
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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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<ResponseEntity<Person>> ludwig = restTemplate.getForEntity("/composers/{id}", Person.class, 42);
ListenableFuture<ResponseEntity<Person>> 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<ResponseEntity<Person>> ludwig = restTemplate.getForEntity("/composers/{id}", Person.class, 42);
ListenableFuture<ResponseEntity<Person>> 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<ResponseEntity<Person>> ludwig = restTemplate.getForEntity("/composers/{id}", Person.class, 42);
ListenableFuture<ResponseEntity<Person>> 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)"));
}
}
}

View File

@ -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)"));
}
}
}

View File

@ -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 ...

View File

@ -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.