Using MockRestServiceServer to Test a REST Client
This article is an example in using MockRestServiceServer
to test a REST client. MockRestServiceServer
is a part of the Spring library for testing. It is the main entry point for client-side REST testing. It provides mock responses from expected requests through the RestTemplate
. It eliminates the use of an actual server and thus speeds up the testing process.
With its operations, we can verify that all requests were performed. We can reset the internal state thus removing all expectations and requests. We can set up an expectation for single or multiple HTTP requests.
1. Assumptions
This article goes straight into discussing about using MockRestServiceServer
to test a REST client. It is assumed that the reader is familiar with some of the following concepts:
- Spring
- Mars Eclipse or any IDE
- JUnit
- Apache Maven
2. Code Examples
ExampleRestService
is found in the com.javacodegeeks.example.service
main package.
ExampleRestService.java
package com.javacodegeeks.example.service; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestTemplate; @Service public class ExampleRestService { @Autowired private RestTemplate restTemplate; public String getRootResource() { String result = restTemplate.getForObject("http://localhost:8080", String.class); System.out.println("getRootResource: " + result); return result; } public String addComment(String comment) { String result = null; try { result = restTemplate.postForObject("http://localhost/add-comment", comment, String.class); System.out.println("addComment: " + result); } catch (HttpClientErrorException e) { result = e.getMessage(); } return result; } }
The ExampleRestService
class is a service layer class. A common layer that uses the service layer is the presentation layer. There are two services provided by ExampleRestService
, getRootResource
and addComment
. The getRootResource
operation communicates to a URL using RestTemplate
and returns the result to the presentation layer. The second operation named addComment
accepts a comment from the presentation layer then posts it to a URL via RestTemplate
. It then returns a result to the presentation layer specifying whether it was successful or not. Just imagine that the addComment
operation is adding a comment on a Facebook post.
To test ExampleRestService
, we have the ExampleRestServiceTestViaRestGateway
and ExampleRestServiceTest
in the com.javacodegeeks.example.service
test package.
ExampleRestServiceTestViaRestGateway.java
package com.javacodegeeks.example.service; import static org.junit.Assert.assertEquals; import static org.springframework.test.web.client.ExpectedCount.once; import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.client.MockRestServiceServer; import org.springframework.web.client.RestTemplate; import org.springframework.web.client.support.RestGatewaySupport; import com.javacodegeeks.example.service.ExampleRestService; @RunWith(SpringRunner.class) @SpringBootTest public class ExampleRestServiceTestViaRestGateway { @Autowired RestTemplate restTemplate; @Autowired ExampleRestService service; private MockRestServiceServer mockServer; @Before public void setUp() { RestGatewaySupport gateway = new RestGatewaySupport(); gateway.setRestTemplate(restTemplate); mockServer = MockRestServiceServer.createServer(gateway); } @Test public void testGetRootResourceOnce() { mockServer.expect(once(), requestTo("http://localhost:8080")) .andRespond(withSuccess("{message : 'under construction'}", MediaType.APPLICATION_JSON)); String result = service.getRootResource(); System.out.println("testGetRootResourceOnce: " + result); mockServer.verify(); assertEquals("{message : 'under construction'}", result); } }
The ExampleRestServiceTestViaRestGateway
class will test the ExampleRestService
class by mocking a REST server. As shown in ExampleRestService
, the REST server is localhost. Instead of running an actual REST server, a mock server is used to mimic the actual REST server. The fake server is created using Spring’s MockRestServiceServer
. This makes testing cheap and fast. Regression testing can be done several times a day.
The @RunWith
annotation means that instead of using the built-in JUnit test runner, SpringRunner.class
will become the test runner. SpringRunner
is the new name of SpringJUnit4ClassRunner
. The @SpringBootTest
means to add Spring Boot support to the test (e.g. no XML configuration). The @Autowired
annotation tells Spring where an injection should occur. For example, Spring will automatically create a bean of type RestTemplate
and inject it to the restTemplate
field. The @Before
annotation tells the test runner that it should be called before every test. For every test a new RestGatewaySupport
is created and a new MockRestServiceServer
is also created. The @Test
annotation on a method marks it as a single test. This is the actual test. The testGetRootResourceOnce
mocks the REST server ExampleRestService
will be communicating to via RestTemplate
. This test expects that ExampleRestService
will call the REST server (HTTP request) only once. It expects the URL of the REST server to be http://localhost:8080 and will respond successfully with a JSON message. If these expectations are not met, the verify method will fail this test.
The line “String result = service.getRootResource();”
tests the getRootResource
method of ExampleRestService
. The assertEquals
line below checks if the expected result is equivalent to the actual result returned by the getRootResource
method. The test passes if it is the same.
ExampleRestServiceTest.java
package com.javacodegeeks.example.service; import static org.junit.Assert.assertEquals; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.client.MockRestServiceServer; import org.springframework.web.client.RestTemplate; import com.javacodegeeks.example.service.ExampleRestService; import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; import static org.springframework.test.web.client.match.MockRestRequestMatchers.method; import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; import static org.springframework.test.web.client.response.MockRestResponseCreators.withBadRequest; import static org.springframework.test.web.client.ExpectedCount.once; import static org.springframework.test.web.client.ExpectedCount.times; @RunWith(SpringRunner.class) //SpringRunner is an alias for the SpringJUnit4ClassRunner @SpringBootTest public class ExampleRestServiceTest { @Autowired RestTemplate restTemplate; @Autowired ExampleRestService service; private MockRestServiceServer mockServer; @Before public void setUp() { mockServer = MockRestServiceServer.createServer(restTemplate); } @Test public void testGetRootResource() { mockServer.expect(requestTo("http://localhost:8080")).andRespond(withSuccess("hello", MediaType.TEXT_PLAIN)); String result = service.getRootResource(); System.out.println("testGetRootResource: " + result); mockServer.verify(); assertEquals("hello", result); } @Test public void testGetRootResourceOnce() { mockServer.expect(once(), requestTo("http://localhost:8080")) .andRespond(withSuccess("{message : 'under construction'}", MediaType.APPLICATION_JSON)); String result = service.getRootResource(); System.out.println("testGetRootResourceOnce: " + result); mockServer.verify(); assertEquals("{message : 'under construction'}", result); } @Test public void testGetRootResourceTimes() { mockServer.expect(times(2), requestTo("http://localhost:8080")) .andRespond(withSuccess("{message : 'under construction'}", MediaType.APPLICATION_JSON)); String result = service.getRootResource(); System.out.println("testGetRootResourceTimes: " + result); mockServer.verify(); // should fail because this test expects RestTemplate.getForObject to be called twice assertEquals("{message : 'under construction'}", result); } @Test public void testAddComment() { mockServer.expect(requestTo("http://localhost/add-comment")).andExpect(method(HttpMethod.POST)) .andRespond(withSuccess("{post : 'success'}", MediaType.APPLICATION_JSON)); String result = service.addComment("cute puppy"); System.out.println("testAddComment: " + result); mockServer.verify(); assertEquals("{post : 'success'}", result); } @Test public void testAddCommentClientError() { mockServer.expect(requestTo("http://localhost/add-comment")).andExpect(method(HttpMethod.POST)) .andRespond(withBadRequest()); String result = service.addComment("cute puppy"); System.out.println("testAddCommentClientError: " + result); mockServer.verify(); assertEquals("400 Bad Request", result); } @Test public void testReset() { mockServer.expect(requestTo("http://localhost/add-comment")).andExpect(method(HttpMethod.POST)) .andRespond(withSuccess("{post : 'success'}", MediaType.APPLICATION_JSON)); String result = service.addComment("cute puppy"); System.out.println("testReset 1st: " + result); mockServer.verify(); assertEquals("{post : 'success'}", result); mockServer.reset(); mockServer.expect(requestTo("http://localhost:8080")).andRespond(withSuccess("hello", MediaType.TEXT_PLAIN)); result = service.getRootResource(); System.out.println("testReset 2nd: " + result); mockServer.verify(); assertEquals("hello", result); } }
The ExampleRestServiceTest
also tests the ExampleRestService
. The difference with the earlier example is that this test uses RestTemplate
to create the server instead of RestGateWaySupport
. The annotations used in this class are the same as the previous example. The class contains six tests and one of the test is designed to fail.
In the testGetRootResource
method, if the expected count (e.g., once()) is not specified then by default it expects a single HTTP request.
The testGetRootResourceTimes
will fail because it expects two HTTP requests to the REST server but ExampleRestService
only invokes RestTemplate’s getForObject
method once.
The testAddComment
method simulates an HTTP POST request. The mock server has two expectations, a specified URL and certain HTTP request method.
In testAddCommentClientError
, a client error is simulated. The mock server returns an HTTP status code signifying an HTTP client error (e.g., malformed request).
In the testReset
method, the service is called twice. The mock server didn’t fail the test because a reset is made (i.e., mockServer.reset()
is invoked between each single call). The MockRestServiceServer
reset operation removes all expectations and recorded requests.
3. Async Code Examples
AsyncExampleRestService
is found in the com.javacodegeeks.example.service
main package.
AsyncExampleRestService.java
package com.javacodegeeks.example.service; import java.util.concurrent.ExecutionException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.concurrent.ListenableFuture; import org.springframework.web.client.AsyncRestTemplate; import org.springframework.web.client.HttpServerErrorException; @Service public class AsyncExampleRestService { @Autowired private AsyncRestTemplate asyncRestTemplate; public String deleteAllSuspendedUsers() { ListenableFuture future = asyncRestTemplate.delete("http://localhost/delete-all-suspended-users"); // doing some long process here... Object result = null; String returnValue = ""; try { result = future.get(); //The Future will return a null result upon completion. if (result == null) { returnValue = "{result:'success'}"; } else { returnValue = "{result:'fail'}"; } } catch (InterruptedException | ExecutionException e) { if (e.getCause() instanceof HttpServerErrorException) { returnValue = "{result: 'server error'}"; } } System.out.println("deleteAllSuspendedUsers: " + result); return returnValue; } }
AsyncExampleRestService
uses Spring’s AsyncRestTemplate
class to access a REST server. AsyncRestTemplate
is similar to RestTemplate
and is used for asynchronous client-side HTTP access. The operation in this service is simulating a delete of all suspended users which might take a significant amount of time. That is why we’re doing it asynchronously. It doesn’t need to wait or block to perform the next lines of code. The future.get()
statement blocks and returns null
if the AsyncRestTemplate.delete
call is finished or throws an Exception
when something went wrong.
To test AsyncExampleRestService
, we have the AsyncExampleRestServiceTest
in the com.javacodegeeks.example.service
test package.
AsyncExampleRestServiceTest.java
package com.javacodegeeks.example.service; import static org.junit.Assert.assertEquals; 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.withServerError; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.HttpMethod; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.client.MockRestServiceServer; import org.springframework.web.client.AsyncRestTemplate; import com.javacodegeeks.example.service.AsyncExampleRestService; @RunWith(SpringRunner.class) @SpringBootTest public class AsyncExampleRestServiceTest { @Autowired AsyncRestTemplate asyncRestTemplate; @Autowired AsyncExampleRestService service; private MockRestServiceServer mockServer; @Before public void setUp() { mockServer = MockRestServiceServer.createServer(asyncRestTemplate); } @Test public void testDeleteAllSuspendedUsers() { mockServer.expect(requestTo("http://localhost/delete-all-suspended-users")).andExpect(method(HttpMethod.DELETE)) .andRespond(withServerError()); String result = service.deleteAllSuspendedUsers(); System.out.println("testDeleteAllSuspendedUsers: " + result); mockServer.verify(); assertEquals("{result: 'server error'}", result); } }
The testDeleteAllSuspendedUsers
method is similar to the other test methods. The difference is the mock server expects an HTTP DELETE request and a server error response. Instead of an HTTP status code 5xx (e.g., 500 – Internal Server Error) to indicate a server error, the service layer returns a JSON string. The HttpServerErrorException
was handled by AsyncExampleRestService
and in turn returned a JSON string to signify the error. The withServerError
call triggers the fake server to generate a server error.
4. Run the Tests
The test output should look like the one below:
ExampleRestServiceTest Output
. ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v1.5.1.RELEASE) 2017-09-07 14:10:49.438 INFO 7916 --- [ main] c.j.e.service.ExampleRestServiceTest : Starting ExampleRestServiceTest on asus_k43s with PID 7916 (started by jpllosa in D:\javacodegeeks_com\mockrestserviceserver\mockrestserviceserver-example) 2017-09-07 14:10:49.441 INFO 7916 --- [ main] c.j.e.service.ExampleRestServiceTest : No active profile set, falling back to default profiles: default 2017-09-07 14:10:49.621 INFO 7916 --- [ main] o.s.w.c.s.GenericWebApplicationContext : Refreshing org.springframework.web.context.support.GenericWebApplicationContext@22fcf7ab: startup date [Thu Sep 07 14:10:49 BST 2017]; root of context hierarchy 2017-09-07 14:10:52.386 INFO 7916 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration' of type [class org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying) 2017-09-07 14:10:52.567 INFO 7916 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean 'validator' of type [class org.springframework.validation.beanvalidation.LocalValidatorFactoryBean] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying) 2017-09-07 14:10:54.738 INFO 7916 --- [ main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.web.context.support.GenericWebApplicationContext@22fcf7ab: startup date [Thu Sep 07 14:10:49 BST 2017]; root of context hierarchy 2017-09-07 14:10:55.028 INFO 7916 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/]}" onto public java.lang.String com.javacodegeeks.example.MockRestServiceServerExample.getRootResource() 2017-09-07 14:10:55.048 INFO 7916 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest) 2017-09-07 14:10:55.052 INFO 7916 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse) 2017-09-07 14:10:55.237 INFO 7916 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler] 2017-09-07 14:10:55.238 INFO 7916 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler] 2017-09-07 14:10:55.392 INFO 7916 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler] 2017-09-07 14:10:55.839 INFO 7916 --- [ main] c.j.e.service.ExampleRestServiceTest : Started ExampleRestServiceTest in 7.163 seconds (JVM running for 9.03) getRootResource: {message : 'under construction'} testGetRootResourceOnce: {message : 'under construction'} addComment: {post : 'success'} testReset 1st: {post : 'success'} getRootResource: hello testReset 2nd: hello testAddCommentClientError: 400 Bad Request getRootResource: hello testGetRootResource: hello getRootResource: {message : 'under construction'} testGetRootResourceTimes: {message : 'under construction'} addComment: {post : 'success'} testAddComment: {post : 'success'} 2017-09-07 14:10:56.235 INFO 7916 --- [ Thread-3] o.s.w.c.s.GenericWebApplicationContext : Closing org.springframework.web.context.support.GenericWebApplicationContext@22fcf7ab: startup date [Thu Sep 07 14:10:49 BST 2017]; root of context hierarchy
5. Using MockRestServiceServer to Test a REST Client Summary
MockRestServiceServer
is used for testing the client side. We should create an instance of it by using an instance of RestTemplate
that is being used by our production code. We don’t create a new RestTemplate
in our test.
After each test, the verify
method must be called after the RestTemplate
is called to run the MockRestServiceServer
assertions.
6. Download the Source Code
This is an example of using MockRestServiceServer
to test a REST client.
You can download the source code of this example here: mockrestserviceserver-example.zip.
In my production code I need to execute POST command to a controller which response StreamingResponseBody. In the testing I would like to mock the response from this controller. How do I respond with StreamingResponseBody in MockRestServiceServer? An example to the code can be found at https://stackoverflow.com/questions/61045521/how-to-mock-streamingresponsebody-in-testing
Hi Ida,
Saw your SO question. So you want to test DalaLakeRealController. MockRestServiceServer is used to test a REST client. It looks like you want to test a REST service and not a REST client. Take a look at Spring’s MockMvc, I think this is what you need.
Cheers,
Joel
It gives me error 404 error. Any fixes/suggestions ?
Which bit gives the 404 error? What resource/page can’t be found?
Thank you bro,