From 37a3fa96d19e4e46869b50e03185ceaad35c37da Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 22 Feb 2016 15:41:36 -0500 Subject: [PATCH 1/5] Separate ResponseActions from ClientHttpRequest Before this commit RequestMatcherClientHttpRequest served both as API to define request expectations, i.e. ResponseActions, as well as the implementation of ClientHttpRequest representing actual requests. DefaultResponseActions replaces this class as a simple holder of expected requests and mock responses. MockRestServiceServer is then responsible to match request expectations and create a mock response. Issue: SPR-11365 --- ...quest.java => DefaultResponseActions.java} | 37 +++++------ .../web/client/MockRestServiceServer.java | 63 ++++++++++++------- .../MockClientHttpRequestFactoryTests.java | 2 - 3 files changed, 56 insertions(+), 46 deletions(-) rename spring-test/src/main/java/org/springframework/test/web/client/{RequestMatcherClientHttpRequest.java => DefaultResponseActions.java} (56%) 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/DefaultResponseActions.java similarity index 56% rename from spring-test/src/main/java/org/springframework/test/web/client/RequestMatcherClientHttpRequest.java rename to spring-test/src/main/java/org/springframework/test/web/client/DefaultResponseActions.java index 1a01328bdd..9cccf6d925 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/RequestMatcherClientHttpRequest.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/DefaultResponseActions.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 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. @@ -20,28 +20,26 @@ 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.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. + * Default implementation of {@code ResponseActions} that is also a composite + * {@code RequestMatcher}, invoking all request matchers it contains, as well as + * a {@code ResponseCreator} delegating to the response creator it contains. * - * @author Craig Walls * @author Rossen Stoyanchev - * @since 3.2 + * @since 4.3 */ -class RequestMatcherClientHttpRequest extends MockAsyncClientHttpRequest implements ResponseActions { +class DefaultResponseActions implements ResponseActions, RequestMatcher, ResponseCreator { private final List requestMatchers = new LinkedList(); private ResponseCreator responseCreator; - public RequestMatcherClientHttpRequest(RequestMatcher requestMatcher) { + public DefaultResponseActions(RequestMatcher requestMatcher) { Assert.notNull(requestMatcher, "RequestMatcher is required"); this.requestMatchers.add(requestMatcher); } @@ -61,21 +59,18 @@ class RequestMatcherClientHttpRequest extends MockAsyncClientHttpRequest impleme } @Override - public ClientHttpResponse executeInternal() throws IOException { - if (this.requestMatchers.isEmpty()) { - throw new AssertionError("No request expectations to execute"); + public void match(ClientHttpRequest request) throws IOException { + for (RequestMatcher matcher : this.requestMatchers) { + matcher.match(request); } + } + @Override + public ClientHttpResponse createResponse(ClientHttpRequest request) throws IOException { 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())"); + throw new IllegalStateException("createResponse called before ResponseCreator was set."); } - - for (RequestMatcher requestMatcher : this.requestMatchers) { - requestMatcher.match(this); - } - setResponse(this.responseCreator.createResponse(this)); - return super.executeInternal(); + return this.responseCreator.createResponse(request); } } 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..b2577d2ca0 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 @@ -27,6 +27,8 @@ 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; @@ -96,11 +98,11 @@ import org.springframework.web.client.support.RestGatewaySupport; */ public class MockRestServiceServer { - private final List expectedRequests = - new LinkedList(); + private final List responseActions = + new LinkedList(); - private final List actualRequests = - new LinkedList(); + private final List requests = + new LinkedList(); /** @@ -161,9 +163,9 @@ public class MockRestServiceServer { * @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); + Assert.state(this.requests.isEmpty(), "Can't add more expected requests with test already underway"); + DefaultResponseActions request = new DefaultResponseActions(requestMatcher); + this.responseActions.add(request); return request; } @@ -173,7 +175,7 @@ public class MockRestServiceServer { * @throws AssertionError when some expectations were not met */ public void verify() { - if (this.expectedRequests.isEmpty() || this.expectedRequests.equals(this.actualRequests)) { + if (this.responseActions.isEmpty() || this.responseActions.size() == this.requests.size()) { return; } throw new AssertionError(getVerifyMessage()); @@ -181,15 +183,15 @@ public class MockRestServiceServer { private String getVerifyMessage() { StringBuilder sb = new StringBuilder("Further request(s) expected\n"); - if (this.actualRequests.size() > 0) { + if (this.requests.size() > 0) { sb.append("The following "); } - sb.append(this.actualRequests.size()).append(" out of "); - sb.append(this.expectedRequests.size()).append(" were executed"); + sb.append(this.requests.size()).append(" out of "); + sb.append(this.responseActions.size()).append(" were executed"); - if (this.actualRequests.size() > 0) { + if (this.requests.size() > 0) { sb.append(":\n"); - for (RequestMatcherClientHttpRequest request : this.actualRequests) { + for (MockAsyncClientHttpRequest request : this.requests) { sb.append(request.toString()).append("\n"); } } @@ -199,12 +201,12 @@ public class MockRestServiceServer { /** * Mock ClientHttpRequestFactory that creates requests by iterating - * over the list of expected {@link RequestMatcherClientHttpRequest}'s. + * over the list of expected {@link DefaultResponseActions}'s. */ private class RequestMatcherClientHttpRequestFactory implements ClientHttpRequestFactory, AsyncClientHttpRequestFactory { - private Iterator requestIterator; + private Iterator requestIterator; @Override public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException { @@ -216,23 +218,38 @@ public class MockRestServiceServer { return createRequestInternal(uri, httpMethod); } - private RequestMatcherClientHttpRequest createRequestInternal(URI uri, HttpMethod httpMethod) { + private MockAsyncClientHttpRequest createRequestInternal(URI uri, HttpMethod httpMethod) { Assert.notNull(uri, "'uri' must not be null"); Assert.notNull(httpMethod, "'httpMethod' must not be null"); + MockAsyncClientHttpRequest request = new MockAsyncClientHttpRequest(httpMethod, uri) { + @Override + protected ClientHttpResponse executeInternal() throws IOException { + ClientHttpResponse response = validateRequest(this); + setResponse(response); + return response; + } + }; + + MockRestServiceServer.this.requests.add(request); + return request; + } + + private ClientHttpResponse validateRequest(MockAsyncClientHttpRequest request) + throws IOException { + if (this.requestIterator == null) { - this.requestIterator = MockRestServiceServer.this.expectedRequests.iterator(); + this.requestIterator = MockRestServiceServer.this.responseActions.iterator(); } if (!this.requestIterator.hasNext()) { - throw new AssertionError("No further requests expected: HTTP " + httpMethod + " " + uri); + throw new AssertionError("No further requests expected: HTTP " + + request.getMethod() + " " + request.getURI()); } - RequestMatcherClientHttpRequest request = this.requestIterator.next(); - request.setURI(uri); - request.setMethod(httpMethod); + DefaultResponseActions responseActions = this.requestIterator.next(); + responseActions.match(request); - MockRestServiceServer.this.actualRequests.add(request); - return request; + return responseActions.createResponse(request); } } 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 index dfd3a7147e..c5ae4fe364 100644 --- 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 @@ -52,10 +52,8 @@ public class MockClientHttpRequestFactoryTests { @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()); } From f58ef24efdb08a3b411b32b8b7ce34547f8b220a Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 22 Feb 2016 21:44:23 -0500 Subject: [PATCH 2/5] Introduce RequestExpectationManager This commit factors out the logic to declare and manage expectations including matching them to requests and verifying at the end behind a commong abstraction. MockRestServiceServer delegates to the new abstraction and is no longer aware of how that's done. There are two implementations, one for ordered and another for unordered expectation. Issue: SPR-11365 --- .../AbstractRequestExpectationManager.java | 79 ++++++++++++++ .../web/client/DefaultResponseActions.java | 2 +- .../web/client/MockRestServiceServer.java | 94 +++++----------- .../OrderedRequestExpectationManager.java | 78 ++++++++++++++ .../test/web/client/RequestExpectation.java | 27 +++++ .../web/client/RequestExpectationManager.java | 68 ++++++++++++ .../UnorderedRequestExpectationManager.java | 93 ++++++++++++++++ .../MockClientHttpRequestFactoryTests.java | 100 ------------------ ...OrderedRequestExpectationManagerTests.java | 90 ++++++++++++++++ 9 files changed, 464 insertions(+), 167 deletions(-) create mode 100644 spring-test/src/main/java/org/springframework/test/web/client/AbstractRequestExpectationManager.java create mode 100644 spring-test/src/main/java/org/springframework/test/web/client/OrderedRequestExpectationManager.java create mode 100644 spring-test/src/main/java/org/springframework/test/web/client/RequestExpectation.java create mode 100644 spring-test/src/main/java/org/springframework/test/web/client/RequestExpectationManager.java create mode 100644 spring-test/src/main/java/org/springframework/test/web/client/UnorderedRequestExpectationManager.java delete mode 100644 spring-test/src/test/java/org/springframework/test/web/client/MockClientHttpRequestFactoryTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/web/client/OrderedRequestExpectationManagerTests.java 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..242ab77c59 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/client/AbstractRequestExpectationManager.java @@ -0,0 +1,79 @@ +/* + * 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; + +/** + * Base class for {@code RequestExpectationManager} implementations. + * Creates and contains expectations and stores actual requests. + * + *

Sub-classes are responsible for matching actual to expected requests and + * for verifying remaining expectations at the end. + * + * @author Rossen Stoyanchev + * @since 4.3 + */ +public abstract class AbstractRequestExpectationManager implements RequestExpectationManager { + + private final List expectations = new LinkedList(); + + private final List requests = new LinkedList(); + + + public AbstractRequestExpectationManager() { + } + + public AbstractRequestExpectationManager(List expectations) { + this.expectations.addAll(expectations); + } + + + @Override + public List getExpectations() { + return this.expectations; + } + + @Override + public List getRequests() { + return this.requests; + } + + @Override + public ResponseActions expectRequest(RequestMatcher requestMatcher) { + Assert.state(getRequests().isEmpty(), "Cannot add more expectations after actual requests are made."); + DefaultResponseActions expectation = new DefaultResponseActions(requestMatcher); + getExpectations().add(expectation); + return expectation; + } + + @Override + public ClientHttpResponse validateRequest(ClientHttpRequest request) throws IOException { + ClientHttpResponse response = validateRequestInternal(request); + getRequests().add(request); + return response; + } + + protected abstract ClientHttpResponse validateRequestInternal(ClientHttpRequest request) + throws IOException; + +} diff --git a/spring-test/src/main/java/org/springframework/test/web/client/DefaultResponseActions.java b/spring-test/src/main/java/org/springframework/test/web/client/DefaultResponseActions.java index 9cccf6d925..4b76aa1ac9 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/DefaultResponseActions.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/DefaultResponseActions.java @@ -32,7 +32,7 @@ import org.springframework.util.Assert; * @author Rossen Stoyanchev * @since 4.3 */ -class DefaultResponseActions implements ResponseActions, RequestMatcher, ResponseCreator { +class DefaultResponseActions implements ResponseActions, RequestExpectation { private final List requestMatchers = new LinkedList(); 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 b2577d2ca0..0c31c74cb4 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,8 +18,6 @@ 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; @@ -98,11 +96,7 @@ import org.springframework.web.client.support.RestGatewaySupport; */ public class MockRestServiceServer { - private final List responseActions = - new LinkedList(); - - private final List requests = - new LinkedList(); + private RequestExpectationManager expectationManager = new OrderedRequestExpectationManager(); /** @@ -123,7 +117,7 @@ public class MockRestServiceServer { public static MockRestServiceServer createServer(RestTemplate restTemplate) { Assert.notNull(restTemplate, "'restTemplate' must not be null"); MockRestServiceServer mockServer = new MockRestServiceServer(); - RequestMatcherClientHttpRequestFactory factory = mockServer.new RequestMatcherClientHttpRequestFactory(); + MockClientHttpRequestFactory factory = mockServer.new MockClientHttpRequestFactory(); restTemplate.setRequestFactory(factory); return mockServer; } @@ -137,7 +131,7 @@ public class MockRestServiceServer { public static MockRestServiceServer createServer(AsyncRestTemplate asyncRestTemplate) { Assert.notNull(asyncRestTemplate, "'asyncRestTemplate' must not be null"); MockRestServiceServer mockServer = new MockRestServiceServer(); - RequestMatcherClientHttpRequestFactory factory = mockServer.new RequestMatcherClientHttpRequestFactory(); + MockClientHttpRequestFactory factory = mockServer.new MockClientHttpRequestFactory(); asyncRestTemplate.setAsyncRequestFactory(factory); return mockServer; } @@ -154,48 +148,38 @@ public class MockRestServiceServer { } + /** + * When this option is set, the order in which requests are executed does not + * need to match the order in which expected requests are declared. + */ + public MockRestServiceServer setIgnoreRequestOrder() { + String message = "Cannot switch to unordered mode after actual requests are made."; + Assert.state(this.expectationManager.getRequests().isEmpty(), message); + List expectations = this.expectationManager.getExpectations(); + this.expectationManager = new UnorderedRequestExpectationManager(expectations); + return this; + } + /** * 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} + * @param matcher 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.requests.isEmpty(), "Can't add more expected requests with test already underway"); - DefaultResponseActions request = new DefaultResponseActions(requestMatcher); - this.responseActions.add(request); - return request; + public ResponseActions expect(RequestMatcher matcher) { + return this.expectationManager.expectRequest(matcher); } + /** * Verify that all expected requests set up via * {@link #expect(RequestMatcher)} were indeed performed. * @throws AssertionError when some expectations were not met */ public void verify() { - if (this.responseActions.isEmpty() || this.responseActions.size() == this.requests.size()) { - return; - } - throw new AssertionError(getVerifyMessage()); - } - - private String getVerifyMessage() { - StringBuilder sb = new StringBuilder("Further request(s) expected\n"); - if (this.requests.size() > 0) { - sb.append("The following "); - } - sb.append(this.requests.size()).append(" out of "); - sb.append(this.responseActions.size()).append(" were executed"); - - if (this.requests.size() > 0) { - sb.append(":\n"); - for (MockAsyncClientHttpRequest request : this.requests) { - sb.append(request.toString()).append("\n"); - } - } - return sb.toString(); + this.expectationManager.verify(); } @@ -203,53 +187,31 @@ public class MockRestServiceServer { * Mock ClientHttpRequestFactory that creates requests by iterating * over the list of expected {@link DefaultResponseActions}'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 MockAsyncClientHttpRequest 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"); + + return new MockAsyncClientHttpRequest(method, uri) { - MockAsyncClientHttpRequest request = new MockAsyncClientHttpRequest(httpMethod, uri) { @Override protected ClientHttpResponse executeInternal() throws IOException { - ClientHttpResponse response = validateRequest(this); + ClientHttpResponse response = expectationManager.validateRequest(this); setResponse(response); return response; } }; - - MockRestServiceServer.this.requests.add(request); - return request; - } - - private ClientHttpResponse validateRequest(MockAsyncClientHttpRequest request) - throws IOException { - - if (this.requestIterator == null) { - this.requestIterator = MockRestServiceServer.this.responseActions.iterator(); - } - if (!this.requestIterator.hasNext()) { - throw new AssertionError("No further requests expected: HTTP " + - request.getMethod() + " " + request.getURI()); - } - - DefaultResponseActions responseActions = this.requestIterator.next(); - responseActions.match(request); - - return responseActions.createResponse(request); } } diff --git a/spring-test/src/main/java/org/springframework/test/web/client/OrderedRequestExpectationManager.java b/spring-test/src/main/java/org/springframework/test/web/client/OrderedRequestExpectationManager.java new file mode 100644 index 0000000000..12437e4a9a --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/client/OrderedRequestExpectationManager.java @@ -0,0 +1,78 @@ +/* + * 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.Iterator; + +import org.springframework.http.HttpMethod; +import org.springframework.http.client.ClientHttpRequest; +import org.springframework.http.client.ClientHttpResponse; + +/** + * {@code RequestExpectationManager} that expects requests to follow the order + * in which expected requests were declared. + * + * @author Rossen Stoyanchev + * @since 4.3 + */ +public class OrderedRequestExpectationManager extends AbstractRequestExpectationManager { + + private Iterator iterator; + + + @Override + public ClientHttpResponse validateRequestInternal(ClientHttpRequest request) throws IOException { + if (this.iterator == null) { + this.iterator = getExpectations().iterator(); + } + if (!this.iterator.hasNext()) { + HttpMethod method = request.getMethod(); + URI uri = request.getURI(); + throw new AssertionError("No further requests expected: HTTP " + method + " " + uri); + } + RequestExpectation expectation = this.iterator.next(); + expectation.match(request); + return expectation.createResponse(request); + } + + @Override + public void verify() { + if (getExpectations().isEmpty() || getExpectations().size() == getRequests().size()) { + return; + } + throw new AssertionError(getVerifyMessage()); + } + + private String getVerifyMessage() { + StringBuilder sb = new StringBuilder("Further request(s) expected\n"); + if (getRequests().size() > 0) { + sb.append("The following "); + } + sb.append(getRequests().size()).append(" out of "); + sb.append(getExpectations().size()).append(" were executed"); + + if (getRequests().size() > 0) { + sb.append(":\n"); + for (ClientHttpRequest request : getRequests()) { + sb.append(request.toString()).append("\n"); + } + } + return sb.toString(); + } + +} 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..cec8c3744b --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/client/RequestExpectation.java @@ -0,0 +1,27 @@ +/* + * 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; + +/** + * A contract that combines {@code RequestMatcher} with {@code ResponseCreator} + * to define an expected request and a response to use for it. + * + * @author Rossen Stoyanchev + * @since 4.3 + */ +public interface RequestExpectation extends RequestMatcher, ResponseCreator { + +} 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..e919a20e02 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/client/RequestExpectationManager.java @@ -0,0 +1,68 @@ +/* + * 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.List; + +import org.springframework.http.client.ClientHttpRequest; +import org.springframework.http.client.ClientHttpResponse; + +/** + * Contract to manage creating HTTP request expectations, apply them to actual + * requests (in strict or random order), and at the end verify whether all + * expectations were met. + * + * @author Rossen Stoyanchev + * @since 4.3 + */ +public interface RequestExpectationManager { + + /** + * Return the list of declared request expectations. + */ + List getExpectations(); + + /** + * Return the list of actual requests. + */ + List getRequests(); + + /** + * 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(RequestMatcher requestMatcher); + + /** + * Validate the given actual request against the declared expectations + * raising {@link AssertionError} if not met. + * @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/UnorderedRequestExpectationManager.java b/spring-test/src/main/java/org/springframework/test/web/client/UnorderedRequestExpectationManager.java new file mode 100644 index 0000000000..2e658d6077 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/client/UnorderedRequestExpectationManager.java @@ -0,0 +1,93 @@ +/* + * 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.LinkedList; +import java.util.List; + +import org.springframework.http.HttpMethod; +import org.springframework.http.client.ClientHttpRequest; +import org.springframework.http.client.ClientHttpResponse; + +/** + * {@code RequestExpectationManager} that tries to match actual requests to + * expected requests regardless of the order in which expected requests were + * declared. + * + * @author Rossen Stoyanchev + * @since 4.3 + */ +public class UnorderedRequestExpectationManager extends AbstractRequestExpectationManager { + + private final List remainingExpectations = new LinkedList(); + + + public UnorderedRequestExpectationManager() { + } + + public UnorderedRequestExpectationManager(List expectations) { + super(expectations); + } + + + @Override + public ClientHttpResponse validateRequestInternal(ClientHttpRequest request) throws IOException { + if (getRequests().isEmpty()) { + this.remainingExpectations.addAll(getExpectations()); + } + for (RequestExpectation expectation : getExpectations()) { + try { + expectation.match(request); + this.remainingExpectations.remove(expectation); + return expectation.createResponse(request); + } + catch (AssertionError error) { + // Ignore + } + } + HttpMethod method = request.getMethod(); + URI uri = request.getURI(); + throw new AssertionError("Unexpected request: HTTP " + method + " " + uri); + } + + @Override + public void verify() { + if (getExpectations().isEmpty() || this.remainingExpectations.isEmpty()) { + return; + } + throw new AssertionError(getVerifyMessage()); + } + + private String getVerifyMessage() { + StringBuilder sb = new StringBuilder("Further request(s) expected\n"); + if (getRequests().size() > 0) { + sb.append("The following "); + } + sb.append(getRequests().size()).append(" were executed"); + sb.append(" leaving ").append(this.remainingExpectations.size()).append(" expectations."); + + if (getRequests().size() > 0) { + sb.append(":\n"); + for (ClientHttpRequest request : getRequests()) { + sb.append(request.toString()).append("\n"); + } + } + return sb.toString(); + } + +} 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 c5ae4fe364..0000000000 --- a/spring-test/src/test/java/org/springframework/test/web/client/MockClientHttpRequestFactoryTests.java +++ /dev/null @@ -1,100 +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 actual = this.factory.createRequest(uri, HttpMethod.GET); - - 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/OrderedRequestExpectationManagerTests.java b/spring-test/src/test/java/org/springframework/test/web/client/OrderedRequestExpectationManagerTests.java new file mode 100644 index 0000000000..1a578d3888 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/client/OrderedRequestExpectationManagerTests.java @@ -0,0 +1,90 @@ +/* + * 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.Test; + +import org.springframework.http.HttpMethod; +import org.springframework.http.client.ClientHttpRequest; +import org.springframework.mock.http.client.MockAsyncClientHttpRequest; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.anything; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; + +/** + * Unit tests for {@link AbstractRequestExpectationManager}. + * @author Rossen Stoyanchev + */ +public class OrderedRequestExpectationManagerTests { + + private OrderedRequestExpectationManager manager = new OrderedRequestExpectationManager(); + + + @Test + public void validateWithUnexpectedRequest() throws Exception { + try { + this.manager.validateRequest(request(HttpMethod.GET, "/foo")); + } + catch (AssertionError error) { + assertEquals("No further requests expected: HTTP GET /foo", error.getMessage()); + } + } + + @Test + public void verify() throws Exception { + this.manager.expectRequest(anything()).andRespond(withSuccess()); + this.manager.expectRequest(anything()).andRespond(withSuccess()); + + this.manager.validateRequest(request(HttpMethod.GET, "/foo")); + this.manager.validateRequest(request(HttpMethod.POST, "/bar")); + this.manager.verify(); + } + + @Test + public void verifyWithZeroExpectations() throws Exception { + this.manager.verify(); + } + + @Test + public void verifyWithRemainingExpectations() throws Exception { + this.manager.expectRequest(anything()).andRespond(withSuccess()); + this.manager.expectRequest(anything()).andRespond(withSuccess()); + + this.manager.validateRequest(request(HttpMethod.GET, "/foo")); + try { + this.manager.verify(); + } + catch (AssertionError error) { + assertTrue(error.getMessage(), error.getMessage().contains("1 out of 2 were executed")); + } + } + + private ClientHttpRequest request(HttpMethod method, String url) { + try { + return new MockAsyncClientHttpRequest(method, new URI(url)); + } + catch (URISyntaxException ex) { + throw new IllegalStateException(ex); + } + } + +} From a56c69c9ca410b2a04d946a1199fe1dc10b89f5b Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 23 Feb 2016 07:20:15 -0500 Subject: [PATCH 3/5] Introduce MockRestServiceServer builder MockRestServiceServer now provides static methods for builder-style creation of MockRestServiceServer. This includes an option ignore the order of declaration expected requests. Issue: SPR-11365 --- .../AbstractRequestExpectationManager.java | 2 - .../web/client/MockRestServiceServer.java | 184 +++++++++++++----- .../web/client/RequestExpectationManager.java | 11 -- ...a => SimpleRequestExpectationManager.java} | 2 +- ...OrderedRequestExpectationManagerTests.java | 2 +- 5 files changed, 134 insertions(+), 67 deletions(-) rename spring-test/src/main/java/org/springframework/test/web/client/{OrderedRequestExpectationManager.java => SimpleRequestExpectationManager.java} (96%) diff --git a/spring-test/src/main/java/org/springframework/test/web/client/AbstractRequestExpectationManager.java b/spring-test/src/main/java/org/springframework/test/web/client/AbstractRequestExpectationManager.java index 242ab77c59..d990832133 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/AbstractRequestExpectationManager.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/AbstractRequestExpectationManager.java @@ -48,12 +48,10 @@ public abstract class AbstractRequestExpectationManager implements RequestExpect } - @Override public List getExpectations() { return this.expectations; } - @Override public List getRequests() { return this.requests; } 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 0c31c74cb4..71bae3d573 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,7 +18,6 @@ package org.springframework.test.web.client; import java.io.IOException; import java.net.URI; -import java.util.List; import org.springframework.http.HttpMethod; import org.springframework.http.client.AsyncClientHttpRequest; @@ -96,69 +95,25 @@ import org.springframework.web.client.support.RestGatewaySupport; */ public class MockRestServiceServer { - private RequestExpectationManager expectationManager = new OrderedRequestExpectationManager(); + private final RequestExpectationManager expectationManager; /** * Private constructor. - * @see #createServer(RestTemplate) - * @see #createServer(RestGatewaySupport) + * See static builder methods and {@code createServer} shortcut methods. */ private MockRestServiceServer() { - } - - - /** - * 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 - */ - public static MockRestServiceServer createServer(RestTemplate restTemplate) { - Assert.notNull(restTemplate, "'restTemplate' must not be null"); - MockRestServiceServer mockServer = new MockRestServiceServer(); - MockClientHttpRequestFactory factory = mockServer.new MockClientHttpRequestFactory(); - restTemplate.setRequestFactory(factory); - return mockServer; + this.expectationManager = new SimpleRequestExpectationManager(); } /** - * 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 + * Private constructor with {@code RequestExpectationManager}. + * See static builder methods and {@code createServer} shortcut methods. */ - public static MockRestServiceServer createServer(AsyncRestTemplate asyncRestTemplate) { - Assert.notNull(asyncRestTemplate, "'asyncRestTemplate' must not be null"); - MockRestServiceServer mockServer = new MockRestServiceServer(); - MockClientHttpRequestFactory factory = mockServer.new MockClientHttpRequestFactory(); - asyncRestTemplate.setAsyncRequestFactory(factory); - return mockServer; + private MockRestServiceServer(RequestExpectationManager expectationManager) { + this.expectationManager = expectationManager; } - /** - * 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()); - } - - - /** - * When this option is set, the order in which requests are executed does not - * need to match the order in which expected requests are declared. - */ - public MockRestServiceServer setIgnoreRequestOrder() { - String message = "Cannot switch to unordered mode after actual requests are made."; - Assert.state(this.expectationManager.getRequests().isEmpty(), message); - List expectations = this.expectationManager.getExpectations(); - this.expectationManager = new UnorderedRequestExpectationManager(expectations); - return this; - } /** * Set up a new HTTP request expectation. The returned {@link ResponseActions} @@ -183,6 +138,131 @@ public class MockRestServiceServer { } + /** + * Build a {@code MockRestServiceServer} with a {@code RestTemplate}. + * @since 4.3 + */ + public static MockRestServiceServerBuilder restTemplate(RestTemplate restTemplate) { + return new DefaultBuilder(restTemplate); + } + + /** + * Build a {@code MockRestServiceServer} with an {@code AsyncRestTemplate}. + * @since 4.3 + */ + public static MockRestServiceServerBuilder asyncRestTemplate(AsyncRestTemplate asyncRestTemplate) { + return new DefaultBuilder(asyncRestTemplate); + } + + /** + * Build a {@code MockRestServiceServer} with 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 { + + /** + * When this option is set, requests can be executed in any order, i.e. + * not matching the order in which expected requests are declared. + */ + MockRestServiceServerBuilder ignoreExpectOrder(); + + /** + * 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 boolean ignoreExpectOrder; + + + 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 ignoreExpectOrder() { + this.ignoreExpectOrder = true; + return this; + } + + + @Override + public MockRestServiceServer build() { + + MockRestServiceServer server = (this.ignoreExpectOrder ? + new MockRestServiceServer(new UnorderedRequestExpectationManager()) : + new MockRestServiceServer()); + + MockClientHttpRequestFactory factory = server.new MockClientHttpRequestFactory(); + if (this.restTemplate != null) { + this.restTemplate.setRequestFactory(factory); + } + if (this.asyncRestTemplate != null) { + this.asyncRestTemplate.setAsyncRequestFactory(factory); + } + + return server; + } + } + + /** * Mock ClientHttpRequestFactory that creates requests by iterating * over the list of expected {@link DefaultResponseActions}'s. diff --git a/spring-test/src/main/java/org/springframework/test/web/client/RequestExpectationManager.java b/spring-test/src/main/java/org/springframework/test/web/client/RequestExpectationManager.java index e919a20e02..d15b8094ee 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/RequestExpectationManager.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/RequestExpectationManager.java @@ -16,7 +16,6 @@ package org.springframework.test.web.client; import java.io.IOException; -import java.util.List; import org.springframework.http.client.ClientHttpRequest; import org.springframework.http.client.ClientHttpResponse; @@ -31,16 +30,6 @@ import org.springframework.http.client.ClientHttpResponse; */ public interface RequestExpectationManager { - /** - * Return the list of declared request expectations. - */ - List getExpectations(); - - /** - * Return the list of actual requests. - */ - List getRequests(); - /** * Set up a new request expectation. The returned {@link ResponseActions} is * used to add more expectations and define a response. diff --git a/spring-test/src/main/java/org/springframework/test/web/client/OrderedRequestExpectationManager.java b/spring-test/src/main/java/org/springframework/test/web/client/SimpleRequestExpectationManager.java similarity index 96% rename from spring-test/src/main/java/org/springframework/test/web/client/OrderedRequestExpectationManager.java rename to spring-test/src/main/java/org/springframework/test/web/client/SimpleRequestExpectationManager.java index 12437e4a9a..7a58679778 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/OrderedRequestExpectationManager.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/SimpleRequestExpectationManager.java @@ -30,7 +30,7 @@ import org.springframework.http.client.ClientHttpResponse; * @author Rossen Stoyanchev * @since 4.3 */ -public class OrderedRequestExpectationManager extends AbstractRequestExpectationManager { +public class SimpleRequestExpectationManager extends AbstractRequestExpectationManager { private Iterator iterator; diff --git a/spring-test/src/test/java/org/springframework/test/web/client/OrderedRequestExpectationManagerTests.java b/spring-test/src/test/java/org/springframework/test/web/client/OrderedRequestExpectationManagerTests.java index 1a578d3888..755e4204ad 100644 --- a/spring-test/src/test/java/org/springframework/test/web/client/OrderedRequestExpectationManagerTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/client/OrderedRequestExpectationManagerTests.java @@ -36,7 +36,7 @@ import static org.springframework.test.web.client.response.MockRestResponseCreat */ public class OrderedRequestExpectationManagerTests { - private OrderedRequestExpectationManager manager = new OrderedRequestExpectationManager(); + private SimpleRequestExpectationManager manager = new SimpleRequestExpectationManager(); @Test From 08a08725be865a620373e36ceec0820e36f1fd83 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 23 Feb 2016 10:56:18 -0500 Subject: [PATCH 4/5] Allow plugging in custom RequestExpectationManager The MockRestServiceServer builder now has an option to plug in a custom RequestExpectationManager. Issue: SPR-11365 --- .../AbstractRequestExpectationManager.java | 26 ++++++-------- ...ns.java => DefaultRequestExpectation.java} | 9 +++-- .../web/client/MockRestServiceServer.java | 36 ++++++++++++------- .../test/web/client/RequestExpectation.java | 10 ++++-- .../SimpleRequestExpectationManager.java | 13 +++---- .../UnorderedRequestExpectationManager.java | 16 ++++----- ...SimpleRequestExpectationManagerTests.java} | 5 +-- 7 files changed, 61 insertions(+), 54 deletions(-) rename spring-test/src/main/java/org/springframework/test/web/client/{DefaultResponseActions.java => DefaultRequestExpectation.java} (84%) rename spring-test/src/test/java/org/springframework/test/web/client/{OrderedRequestExpectationManagerTests.java => SimpleRequestExpectationManagerTests.java} (94%) diff --git a/spring-test/src/main/java/org/springframework/test/web/client/AbstractRequestExpectationManager.java b/spring-test/src/main/java/org/springframework/test/web/client/AbstractRequestExpectationManager.java index d990832133..de40942c33 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/AbstractRequestExpectationManager.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/AbstractRequestExpectationManager.java @@ -24,11 +24,11 @@ import org.springframework.http.client.ClientHttpResponse; import org.springframework.util.Assert; /** - * Base class for {@code RequestExpectationManager} implementations. - * Creates and contains expectations and stores actual requests. + * Base class for {@code RequestExpectationManager} implementations responsible + * for storing expectations and requests. * - *

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

Sub-classes are responsible for matching requests to expectations and + * verifying there are no remaining expectations at the end. * * @author Rossen Stoyanchev * @since 4.3 @@ -40,30 +40,26 @@ public abstract class AbstractRequestExpectationManager implements RequestExpect private final List requests = new LinkedList(); - public AbstractRequestExpectationManager() { - } - - public AbstractRequestExpectationManager(List expectations) { - this.expectations.addAll(expectations); - } - - - public List getExpectations() { + protected List getExpectations() { return this.expectations; } - public List getRequests() { + protected List getRequests() { return this.requests; } @Override public ResponseActions expectRequest(RequestMatcher requestMatcher) { Assert.state(getRequests().isEmpty(), "Cannot add more expectations after actual requests are made."); - DefaultResponseActions expectation = new DefaultResponseActions(requestMatcher); + RequestExpectation expectation = createExpectation(requestMatcher); getExpectations().add(expectation); return expectation; } + protected RequestExpectation createExpectation(RequestMatcher requestMatcher) { + return new DefaultRequestExpectation(requestMatcher); + } + @Override public ClientHttpResponse validateRequest(ClientHttpRequest request) throws IOException { ClientHttpResponse response = validateRequestInternal(request); diff --git a/spring-test/src/main/java/org/springframework/test/web/client/DefaultResponseActions.java b/spring-test/src/main/java/org/springframework/test/web/client/DefaultRequestExpectation.java similarity index 84% rename from spring-test/src/main/java/org/springframework/test/web/client/DefaultResponseActions.java rename to spring-test/src/main/java/org/springframework/test/web/client/DefaultRequestExpectation.java index 4b76aa1ac9..45efae1cf5 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/DefaultResponseActions.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/DefaultRequestExpectation.java @@ -25,21 +25,20 @@ import org.springframework.http.client.ClientHttpResponse; import org.springframework.util.Assert; /** - * Default implementation of {@code ResponseActions} that is also a composite - * {@code RequestMatcher}, invoking all request matchers it contains, as well as - * a {@code ResponseCreator} delegating to the response creator it contains. + * Default implementation of {@code RequestExpectation} that simply delegates + * to the request matchers and the response creator it contains. * * @author Rossen Stoyanchev * @since 4.3 */ -class DefaultResponseActions implements ResponseActions, RequestExpectation { +public class DefaultRequestExpectation implements RequestExpectation { private final List requestMatchers = new LinkedList(); private ResponseCreator responseCreator; - public DefaultResponseActions(RequestMatcher requestMatcher) { + public DefaultRequestExpectation(RequestMatcher requestMatcher) { Assert.notNull(requestMatcher, "RequestMatcher is required"); this.requestMatchers.add(requestMatcher); } 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 71bae3d573..82ebcd8a80 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 @@ -200,10 +200,19 @@ public class MockRestServiceServer { public interface MockRestServiceServerBuilder { /** - * When this option is set, requests can be executed in any order, i.e. - * not matching the order in which expected requests are declared. + * 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 ignoreExpectOrder(); + 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 @@ -220,7 +229,7 @@ public class MockRestServiceServer { private final AsyncRestTemplate asyncRestTemplate; - private boolean ignoreExpectOrder; + private RequestExpectationManager expectationManager = new SimpleRequestExpectationManager(); public DefaultBuilder(RestTemplate restTemplate) { @@ -237,19 +246,21 @@ public class MockRestServiceServer { @Override - public MockRestServiceServerBuilder ignoreExpectOrder() { - this.ignoreExpectOrder = true; + 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 = (this.ignoreExpectOrder ? - new MockRestServiceServer(new UnorderedRequestExpectationManager()) : - new MockRestServiceServer()); - + MockRestServiceServer server = new MockRestServiceServer(this.expectationManager); MockClientHttpRequestFactory factory = server.new MockClientHttpRequestFactory(); if (this.restTemplate != null) { this.restTemplate.setRequestFactory(factory); @@ -257,7 +268,6 @@ public class MockRestServiceServer { if (this.asyncRestTemplate != null) { this.asyncRestTemplate.setAsyncRequestFactory(factory); } - return server; } } @@ -265,7 +275,7 @@ public class MockRestServiceServer { /** * Mock ClientHttpRequestFactory that creates requests by iterating - * over the list of expected {@link DefaultResponseActions}'s. + * over the list of expected {@link DefaultRequestExpectation}'s. */ private class MockClientHttpRequestFactory implements ClientHttpRequestFactory, AsyncClientHttpRequestFactory { diff --git a/spring-test/src/main/java/org/springframework/test/web/client/RequestExpectation.java b/spring-test/src/main/java/org/springframework/test/web/client/RequestExpectation.java index cec8c3744b..971139ac1a 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/RequestExpectation.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/RequestExpectation.java @@ -16,12 +16,16 @@ package org.springframework.test.web.client; /** - * A contract that combines {@code RequestMatcher} with {@code ResponseCreator} - * to define an expected request and a response to use for it. + * An extension of {@code ResponseActions} that also implements + * {@code RequestMatcher} and {@code ResponseCreator} + * + *

{@code ResponseActions} is the API for defining expectations while + * {@code RequestExpectation} is the internal SPI to match those expectations + * to actual requests and to create responses. * * @author Rossen Stoyanchev * @since 4.3 */ -public interface RequestExpectation extends RequestMatcher, ResponseCreator { +public interface RequestExpectation extends ResponseActions, RequestMatcher, ResponseCreator { } diff --git a/spring-test/src/main/java/org/springframework/test/web/client/SimpleRequestExpectationManager.java b/spring-test/src/main/java/org/springframework/test/web/client/SimpleRequestExpectationManager.java index 7a58679778..ba37fbb18a 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/SimpleRequestExpectationManager.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/SimpleRequestExpectationManager.java @@ -24,8 +24,8 @@ import org.springframework.http.client.ClientHttpRequest; import org.springframework.http.client.ClientHttpResponse; /** - * {@code RequestExpectationManager} that expects requests to follow the order - * in which expected requests were declared. + * Simple {@code RequestExpectationManager} that matches requests to expectations + * sequentially, i.e. in the order of declaration of expectations. * * @author Rossen Stoyanchev * @since 4.3 @@ -43,7 +43,8 @@ public class SimpleRequestExpectationManager extends AbstractRequestExpectationM if (!this.iterator.hasNext()) { HttpMethod method = request.getMethod(); URI uri = request.getURI(); - throw new AssertionError("No further requests expected: HTTP " + method + " " + uri); + String firstLine = "No further requests expected: HTTP " + method + " " + uri + "\n"; + throw new AssertionError(createErrorMessage(firstLine)); } RequestExpectation expectation = this.iterator.next(); expectation.match(request); @@ -55,11 +56,11 @@ public class SimpleRequestExpectationManager extends AbstractRequestExpectationM if (getExpectations().isEmpty() || getExpectations().size() == getRequests().size()) { return; } - throw new AssertionError(getVerifyMessage()); + throw new AssertionError(createErrorMessage("Further request(s) expected\n")); } - private String getVerifyMessage() { - StringBuilder sb = new StringBuilder("Further request(s) expected\n"); + private String createErrorMessage(String firstLine) { + StringBuilder sb = new StringBuilder(firstLine); if (getRequests().size() > 0) { sb.append("The following "); } diff --git a/spring-test/src/main/java/org/springframework/test/web/client/UnorderedRequestExpectationManager.java b/spring-test/src/main/java/org/springframework/test/web/client/UnorderedRequestExpectationManager.java index 2e658d6077..eab0d4d455 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/UnorderedRequestExpectationManager.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/UnorderedRequestExpectationManager.java @@ -25,9 +25,8 @@ import org.springframework.http.client.ClientHttpRequest; import org.springframework.http.client.ClientHttpResponse; /** - * {@code RequestExpectationManager} that tries to match actual requests to - * expected requests regardless of the order in which expected requests were - * declared. + * {@code RequestExpectationManager} that matches requests to expectations + * regardless of the order of declaration of expectations. * * @author Rossen Stoyanchev * @since 4.3 @@ -37,23 +36,20 @@ public class UnorderedRequestExpectationManager extends AbstractRequestExpectati private final List remainingExpectations = new LinkedList(); - public UnorderedRequestExpectationManager() { - } - - public UnorderedRequestExpectationManager(List expectations) { - super(expectations); + protected List getRemainingExpectations() { + return this.remainingExpectations; } @Override public ClientHttpResponse validateRequestInternal(ClientHttpRequest request) throws IOException { if (getRequests().isEmpty()) { - this.remainingExpectations.addAll(getExpectations()); + getRemainingExpectations().addAll(getExpectations()); } for (RequestExpectation expectation : getExpectations()) { try { expectation.match(request); - this.remainingExpectations.remove(expectation); + getRemainingExpectations().remove(expectation); return expectation.createResponse(request); } catch (AssertionError error) { diff --git a/spring-test/src/test/java/org/springframework/test/web/client/OrderedRequestExpectationManagerTests.java b/spring-test/src/test/java/org/springframework/test/web/client/SimpleRequestExpectationManagerTests.java similarity index 94% rename from spring-test/src/test/java/org/springframework/test/web/client/OrderedRequestExpectationManagerTests.java rename to spring-test/src/test/java/org/springframework/test/web/client/SimpleRequestExpectationManagerTests.java index 755e4204ad..3c649e5b40 100644 --- a/spring-test/src/test/java/org/springframework/test/web/client/OrderedRequestExpectationManagerTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/client/SimpleRequestExpectationManagerTests.java @@ -34,7 +34,7 @@ import static org.springframework.test.web.client.response.MockRestResponseCreat * Unit tests for {@link AbstractRequestExpectationManager}. * @author Rossen Stoyanchev */ -public class OrderedRequestExpectationManagerTests { +public class SimpleRequestExpectationManagerTests { private SimpleRequestExpectationManager manager = new SimpleRequestExpectationManager(); @@ -45,7 +45,8 @@ public class OrderedRequestExpectationManagerTests { this.manager.validateRequest(request(HttpMethod.GET, "/foo")); } catch (AssertionError error) { - assertEquals("No further requests expected: HTTP GET /foo", error.getMessage()); + assertEquals("No further requests expected: HTTP GET /foo\n" + + "0 out of 0 were executed", error.getMessage()); } } From 91872b0d7469a96699181cb37727643450b12a05 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 23 Feb 2016 18:12:32 -0500 Subject: [PATCH 5/5] Add ExpectedCount MockRestServicesServer now supports an expect variant that accepts a range of expected count of executions. Issue: SPR-11365 --- .../AbstractRequestExpectationManager.java | 125 ++++++++++++++-- .../web/client/DefaultRequestExpectation.java | 82 ++++++++++- .../test/web/client/ExpectedCount.java | 117 +++++++++++++++ .../web/client/MockRestServiceServer.java | 107 ++++++-------- .../test/web/client/RequestExpectation.java | 16 ++- .../web/client/RequestExpectationManager.java | 12 +- .../test/web/client/RequestMatcher.java | 3 + .../SimpleRequestExpectationManager.java | 69 +++++---- .../UnorderedRequestExpectationManager.java | 60 ++------ .../DefaultRequestExpectationTests.java | 99 +++++++++++++ .../SimpleRequestExpectationManagerTests.java | 130 +++++++++++++---- ...orderedRequestExpectationManagerTests.java | 133 ++++++++++++++++++ .../web/client/samples/SampleAsyncTests.java | 25 +++- .../test/web/client/samples/SampleTests.java | 30 +++- src/asciidoc/testing.adoc | 5 +- src/asciidoc/whats-new.adoc | 1 + 16 files changed, 808 insertions(+), 206 deletions(-) create mode 100644 spring-test/src/main/java/org/springframework/test/web/client/ExpectedCount.java create mode 100644 spring-test/src/test/java/org/springframework/test/web/client/DefaultRequestExpectationTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/web/client/UnorderedRequestExpectationManagerTests.java diff --git a/spring-test/src/main/java/org/springframework/test/web/client/AbstractRequestExpectationManager.java b/spring-test/src/main/java/org/springframework/test/web/client/AbstractRequestExpectationManager.java index de40942c33..3591784ce4 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/AbstractRequestExpectationManager.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/AbstractRequestExpectationManager.java @@ -16,19 +16,25 @@ package org.springframework.test.web.client; import java.io.IOException; +import java.net.URI; +import java.util.Collection; +import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; +import java.util.Set; +import org.springframework.http.HttpMethod; import org.springframework.http.client.ClientHttpRequest; import org.springframework.http.client.ClientHttpResponse; import org.springframework.util.Assert; /** * Base class for {@code RequestExpectationManager} implementations responsible - * for storing expectations and requests. + * for storing expectations and actual requests, and checking for unsatisfied + * expectations at the end. * - *

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

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

Examples: + *

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

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

Below is an example: *

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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