From 9aa53abdf9a7ca6c8b9c760fe67cb171ca025c85 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Sat, 28 Jun 2014 09:49:19 -0400 Subject: [PATCH] Add AsyncRestTemplate support to client-side MockMvc Issue: SPR-1822 --- .../client/MockAsyncClientHttpRequest.java | 57 ++++++++ .../web/client/MockRestServiceServer.java | 46 ++++-- .../RequestMatcherClientHttpRequest.java | 3 +- .../web/client/samples/SampleAsyncTests.java | 138 ++++++++++++++++++ .../test/web/client/samples/SampleTests.java | 5 +- 5 files changed, 236 insertions(+), 13 deletions(-) create mode 100644 spring-test/src/main/java/org/springframework/mock/http/client/MockAsyncClientHttpRequest.java create mode 100644 spring-test/src/test/java/org/springframework/test/web/client/samples/SampleAsyncTests.java diff --git a/spring-test/src/main/java/org/springframework/mock/http/client/MockAsyncClientHttpRequest.java b/spring-test/src/main/java/org/springframework/mock/http/client/MockAsyncClientHttpRequest.java new file mode 100644 index 0000000000..ef7650bd8f --- /dev/null +++ b/spring-test/src/main/java/org/springframework/mock/http/client/MockAsyncClientHttpRequest.java @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2012 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.mock.http.client; + +import org.springframework.core.task.AsyncListenableTaskExecutor; +import org.springframework.http.HttpMethod; +import org.springframework.http.client.AsyncClientHttpRequest; +import org.springframework.http.client.ClientHttpRequest; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.mock.http.MockHttpOutputMessage; +import org.springframework.util.concurrent.ListenableFuture; +import org.springframework.util.concurrent.SettableListenableFuture; + +import java.io.IOException; +import java.net.URI; +import java.util.concurrent.Callable; + +/** + * An extension of {@link MockClientHttpRequest} that also implements + * {@link AsyncClientHttpRequest} by wraps the response in a "settable" future. + * + * @author Rossen Stoyanchev + * @author Sam Brannen + * @since 3.2 + */ +public class MockAsyncClientHttpRequest extends MockClientHttpRequest implements AsyncClientHttpRequest { + + + public MockAsyncClientHttpRequest() { + } + + public MockAsyncClientHttpRequest(HttpMethod httpMethod, URI uri) { + super(httpMethod, uri); + } + + @Override + public ListenableFuture executeAsync() throws IOException { + SettableListenableFuture future = new SettableListenableFuture(); + future.set(execute()); + return future; + } + +} 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 416f8f3a5e..6691c0ff63 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 @@ -16,17 +16,25 @@ package org.springframework.test.web.client; import java.io.IOException; +import java.io.OutputStream; import java.net.URI; import java.util.Iterator; import java.util.LinkedList; import java.util.List; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; +import org.springframework.http.client.AsyncClientHttpRequest; +import org.springframework.http.client.AsyncClientHttpRequestFactory; import org.springframework.http.client.ClientHttpRequest; import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.ClientHttpResponse; import org.springframework.test.web.client.match.MockRestRequestMatchers; import org.springframework.test.web.client.response.MockRestResponseCreators; import org.springframework.util.Assert; +import org.springframework.util.concurrent.ListenableFuture; +import org.springframework.util.concurrent.SettableListenableFuture; +import org.springframework.web.client.AsyncRestTemplate; import org.springframework.web.client.RestTemplate; import org.springframework.web.client.support.RestGatewaySupport; @@ -80,11 +88,9 @@ import org.springframework.web.client.support.RestGatewaySupport; */ public class MockRestServiceServer { - private final List expectedRequests = - new LinkedList(); + private final List expectedRequests = new LinkedList(); - private final List actualRequests = - new LinkedList(); + private final List actualRequests = new LinkedList(); /** @@ -104,12 +110,24 @@ 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(); - restTemplate.setRequestFactory(factory); + return mockServer; + } + /** + * 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 + */ + public static MockRestServiceServer createServer(AsyncRestTemplate asyncRestTemplate) { + Assert.notNull(asyncRestTemplate, "'asyncRestTemplate' must not be null"); + MockRestServiceServer mockServer = new MockRestServiceServer(); + RequestMatcherClientHttpRequestFactory factory = mockServer.new RequestMatcherClientHttpRequestFactory(); + asyncRestTemplate.setAsyncRequestFactory(factory); return mockServer; } @@ -171,7 +189,6 @@ public class MockRestServiceServer { sb.append(request.toString()).append("\n"); } } - return sb.toString(); } @@ -180,12 +197,22 @@ public class MockRestServiceServer { * Mock ClientHttpRequestFactory that creates requests by iterating * over the list of expected {@link RequestMatcherClientHttpRequest}'s. */ - private class RequestMatcherClientHttpRequestFactory implements ClientHttpRequestFactory { + private class RequestMatcherClientHttpRequestFactory + implements ClientHttpRequestFactory, AsyncClientHttpRequestFactory { private Iterator requestIterator; @Override public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException { + return createRequestInternal(uri, httpMethod); + } + + @Override + public AsyncClientHttpRequest createAsyncRequest(URI uri, HttpMethod httpMethod) throws IOException { + return createRequestInternal(uri, httpMethod); + } + + private RequestMatcherClientHttpRequest createRequestInternal(URI uri, HttpMethod httpMethod) { Assert.notNull(uri, "'uri' must not be null"); Assert.notNull(httpMethod, "'httpMethod' must not be null"); @@ -201,9 +228,8 @@ public class MockRestServiceServer { request.setMethod(httpMethod); MockRestServiceServer.this.actualRequests.add(request); - return request; } } -} +} \ No newline at end of file diff --git a/spring-test/src/main/java/org/springframework/test/web/client/RequestMatcherClientHttpRequest.java b/spring-test/src/main/java/org/springframework/test/web/client/RequestMatcherClientHttpRequest.java index aa923c7b66..c7906178f3 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/RequestMatcherClientHttpRequest.java @@ -20,6 +20,7 @@ import java.util.LinkedList; import java.util.List; import org.springframework.http.client.ClientHttpResponse; +import org.springframework.mock.http.client.MockAsyncClientHttpRequest; import org.springframework.mock.http.client.MockClientHttpRequest; import org.springframework.util.Assert; @@ -33,7 +34,7 @@ import org.springframework.util.Assert; * @author Rossen Stoyanchev * @since 3.2 */ -class RequestMatcherClientHttpRequest extends MockClientHttpRequest implements ResponseActions { +class RequestMatcherClientHttpRequest extends MockAsyncClientHttpRequest implements ResponseActions { private final List requestMatchers = new LinkedList(); 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 new file mode 100644 index 0000000000..d94f6aaae3 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/client/samples/SampleAsyncTests.java @@ -0,0 +1,138 @@ +/* + * 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.samples; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +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.MockRestServiceServer; +import org.springframework.util.concurrent.ListenableFuture; +import org.springframework.web.client.AsyncRestTemplate; + +import static org.junit.Assert.assertTrue; +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; + +/** + * Examples to demonstrate writing client-side REST tests with Spring MVC Test. + * While the tests in this class invoke the RestTemplate directly, in actual + * tests the RestTemplate may likely be invoked indirectly, i.e. through client + * code. + * + * @author Rossen Stoyanchev + */ +public class SampleAsyncTests { + + private MockRestServiceServer mockServer; + + private AsyncRestTemplate restTemplate; + + + @Before + public void setup() { + this.restTemplate = new AsyncRestTemplate(); + this.mockServer = MockRestServiceServer.createServer(this.restTemplate); + + } + + @Test + public void performGet() throws Exception { + + String responseBody = "{\"name\" : \"Ludwig van Beethoven\", \"someDouble\" : \"1.6035\"}"; + + this.mockServer.expect(requestTo("/composers/42")).andExpect(method(HttpMethod.GET)) + .andRespond(withSuccess(responseBody, MediaType.APPLICATION_JSON)); + + @SuppressWarnings("unused") + ListenableFuture> ludwig = 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.mockServer.verify(); + } + + @Test + public void performGetAsync() throws Exception { + + String responseBody = "{\"name\" : \"Ludwig van Beethoven\", \"someDouble\" : \"1.6035\"}"; + + this.mockServer.expect(requestTo("/composers/42")).andExpect(method(HttpMethod.GET)) + .andRespond(withSuccess(responseBody, MediaType.APPLICATION_JSON)); + + @SuppressWarnings("unused") + ListenableFuture> ludwig = restTemplate.getForEntity("/composers/{id}", Person.class, 42); + + // person.getName().equals("Ludwig van Beethoven") + // person.getDouble().equals(1.6035) + + this.mockServer.verify(); + } + + @Test + public void performGetWithResponseBodyFromFile() throws Exception { + + Resource responseBody = new ClassPathResource("ludwig.json", this.getClass()); + + this.mockServer.expect(requestTo("/composers/42")).andExpect(method(HttpMethod.GET)) + .andRespond(withSuccess(responseBody, MediaType.APPLICATION_JSON)); + + @SuppressWarnings("unused") + ListenableFuture> ludwig = restTemplate.getForEntity("/composers/{id}", Person.class, 42); + + // hotel.getId() == 42 + // hotel.getName().equals("Holiday Inn") + + this.mockServer.verify(); + } + + @Test + public void verify() { + + this.mockServer.expect(requestTo("/number")).andExpect(method(HttpMethod.GET)) + .andRespond(withSuccess("1", MediaType.TEXT_PLAIN)); + + this.mockServer.expect(requestTo("/number")).andExpect(method(HttpMethod.GET)) + .andRespond(withSuccess("2", MediaType.TEXT_PLAIN)); + + this.mockServer.expect(requestTo("/number")).andExpect(method(HttpMethod.GET)) + .andRespond(withSuccess("4", MediaType.TEXT_PLAIN)); + + this.mockServer.expect(requestTo("/number")).andExpect(method(HttpMethod.GET)) + .andRespond(withSuccess("8", MediaType.TEXT_PLAIN)); + + @SuppressWarnings("unused") + ListenableFuture> result = this.restTemplate.getForEntity("/number", String.class); + // result == "1" + + result = this.restTemplate.getForEntity("/number", String.class); + // result == "2" + + try { + this.mockServer.verify(); + } + catch (AssertionError error) { + assertTrue(error.getMessage(), error.getMessage().contains("2 out of 4 were executed")); + } + } +} 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 c6f5a0ca7c..73f1c51674 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 @@ -61,8 +61,9 @@ public class SampleTests { @SuppressWarnings("unused") Person ludwig = restTemplate.getForObject("/composers/{id}", Person.class, 42); - // person.getName().equals("Ludwig van Beethoven") - // person.getDouble().equals(1.6035) + // We are only validating the request. The response is mocked out. + // hotel.getId() == 42 + // hotel.getName().equals("Holiday Inn") this.mockServer.verify(); }