In this chapter we will discuss the following:
Building clients using RestTemplate
Spring Test framework basics
Unit testing MVC controllers
Integration testing MVC controllers
We have looked at building REST services using Spring. In this chapter, we will look at building clients that consume these REST services. We will also examine the Spring Test framework that can be used to perform unit and end-to-end testing of REST services.
QuickPoll Java Client
Consuming REST
services involves building a JSON or XML request payload, transmitting the payload via HTTP/HTTPS, and consuming the returned JSON response. This flexibility opens doors to numerous options for building REST clients in Java (or, as a matter of fact, any technology). A straightforward approach for building a Java REST client is to use core JDK libraries. Listing
9-1 shows an example of a client reading a Poll using the QuickPoll REST API.
public void readPoll() {
HttpURLConnection connection = null;
BufferedReader reader = null;
try {
URL restAPIUrl = new URL("http://localhost:8080/v1/polls/1");
connection = (HttpURLConnection) restAPIUrl.openConnection();
connection.setRequestMethod("GET");
reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
StringBuilder jsonData = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
jsonData.append(line);
}
System.out.println(jsonData.toString());
}
catch(Exception e) {
e.printStackTrace();
}
finally {
// Clean up
IOUtils.closeQuietly(reader);
if(connection != null)
connection.disconnect();
}
}
Listing 9-1Reading a Poll Using Java URLClass
Although there is nothing wrong with the approach in Listing 9-1, there is a lot of boilerplate code that needs to be written to perform a simple REST operation. The readPoll method would grow even bigger if we were to include the code to parse the JSON response. Spring abstracts this boilerplate code into templates and utility classes and makes it easy to consume REST services.
RestTemplate
Central to Spring’s support for building REST clients is the org.springframework.web.client.RestTemplate. The RestTemplate takes care of the necessary plumbing needed to communicate with REST services and automatically marshals/unmarshals HTTP request and response bodies. The RestTemplate like Spring’s other popular helper classes such as JdbcTemplate and JmsTemplate is based on the Template Method design pattern.1
The
RestTemplate and associated utility classes are part of the
spring-web.jar file
. If you are building a standalone REST client using
RestTemplate, you need to add the
spring-web dependency, shown in Listing
9-2, to your
pom.xml file.
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.3.9</version>
</dependency>
Listing 9-2Spring-web.jar Dependency
RestTemplate provides convenient methods to perform API requests using six commonly used HTTP methods. In the next sections, we will look at some of these functions along with a generic yet powerful exchange method to build QuickPoll clients.
Getting Polls
RestTemplate provides a
getForObject method to retrieve representations using the GET HTTP method. Listing
9-3 shows the three flavors of the
getForObject method
.
public <T> T getForObject(String url, Class<T> responseType, Object... urlVariables) throws RestClientException {}
public <T> T getForObject(String url, Class<T> responseType, Map<String,?> urlVariables) throws RestClientException
public <T> T getForObject(URI url, Class<T> responseType) throws RestClientException
Listing 9-3GetForObject Method Flavors
The first two methods accept a URI template string, a return value type, and URI variables that can be used to expand the URI template. The third flavor accepts a fully formed URI and return value type. RestTemplate encodes the passed-in URI templates, and, hence, if the URI is already encoded, you must use the third method flavor. Otherwise, it will result in double encoding of the URI, causing malformed URI errors.
Listing
9-4 shows the
QuickPollClient class
and the usage of
getForObject method
to retrieve a Poll for a given poll id. The
QuickPollClient is placed under the
com.apress.client package of our QuickPoll application and is interacting with the first version of our QuickPoll API. In the upcoming sections, we will create clients that interact with second and third versions of the API.
RestTemplate is threadsafe, and, hence, we created a class-level
RestTemplate instance to be used by all client methods. Because we have specified the
Poll.class as the second parameter,
RestTemplate uses
HTTP message converters and automatically converts the HTTP response content into a
Poll instance.
package com.apress.client;
import org.springframework.web.client.RestTemplate;
import com.apress.domain.Poll;
public class QuickPollClient {
private static final String QUICK_POLL_URI_V1 = "http://localhost:8080/v1/polls";
private RestTemplate restTemplate = new RestTemplate();
public Poll getPollById(Long pollId) {
return restTemplate.getForObject(QUICK_POLL_URI_V1 + "/{pollId}", Poll.class, pollId);
}
}
Listing 9-4QuickPollClient and GetForObject Usage
This listing demonstrates the power of
RestTemplate. It took about a dozen lines in Listing
9-1, but we were able to accomplish that and more with a couple of lines using
RestTemplate. The
getPollById method
can be tested with a simple
main method in
QuickPollClient class:
public static void main(String[] args) {
QuickPollClient client = new QuickPollClient();
Poll poll = client.getPollById(1L);
System.out.println(poll);
}
Retrieving a Poll collection resource is a little trickier as providing
List<Poll>.class
as a return value type to the
getForObject would result in compilation error. One approach is to simply specify that we are expecting a collection:
List allPolls = restTemplate.getForObject(QUICK_POLL_URI_V1, List.class);
However, because RestTemplate can’t automatically guess the Java class type of the elements, it would deserialize each JSON object in the returned collection into a LinkedHashMap
. Hence, the call returns all of our Polls as a collection of type List<LinkedHashMap>.
To address this issue, Spring provides a
org.springframework.core.ParameterizedTypeReference abstract class that can capture and retain generic-type information at runtime. So, to specify the fact that we are expecting a list of Poll instances, we create a subclass of
ParameterizedTypeReference
:
ParameterizedTypeReference<List<Poll>> responseType = new ParameterizedTypeReference<List<Poll>>() {};
RestTemplate HTTP-specific methods such as
getForObject
don’t take a
ParameterizedTypeReference as their parameter. As shown in Listing
9-5, we need to use
RestTemplate’s
exchange method in conjunction with
ParameterizedTypeReference. The
exchange method infers the return-type information from the passed-in
responseType parameter and returns a
ResponseEntity instance. Invoking the
getBody method on
ResponseEntity gives us the
Poll collection.
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.ResponseEntity;
import org.springframework.http.HttpMethod;
public List<Poll> getAllPolls() {
ParameterizedTypeReference<List<Poll>> responseType = new ParameterizedTypeReference
<List<Poll>>() {};
ResponseEntity<List<Poll>> responseEntity = restTemplate.exchange(QUICK_POLL_URI_V1, HttpMethod.GET, null, responseType);
List<Poll> allPolls = responseEntity.getBody();
Listing 9-5Get All Polls Using RestTemplate
We can also accomplish similar behavior with
getForObject by requesting
RestTemplate to return an array of
Poll instances:
Poll[] allPolls = restTemplate.getForObject(QUICK_POLL_URI_V1, Poll[].class);
Creating a Poll
RestTemplate provides
two methods—
postForLocation and
postForObject—to perform HTTP POST operations on a resource. Listing
9-6 gives the API for the two methods.
public URI postForLocation(String url, Object request, Object... urlVariables) throws RestClientException
public <T> T postForObject(String url, Object request, Class<T> responseType, Object... uriVariables) throws RestClientException
Listing 9-6RestTemplate’s POST Support
The postForLocation method performs an HTTP POST on the given URI and returns the value of the Location header. As we have seen in our QuickPoll POST implementations, the Location header contains the URI of the newly created resource. The postForObject works similar to postForLocation but converts a response into a representation. The responseType parameter indicates the type of representation to be expected.
Listing
9-7 shows the
QuickPollClient’s
createPoll method that creates a new
Poll using the
postForLocation method.
public URI createPoll(Poll poll) {
return restTemplate.postForLocation( QUICK_POLL_URI_V1, poll);
}
Listing 9-7Create a Poll Using PostForLocation
Update the
QuickPollClient’s
main method with this code to test the
createPoll method:
public static void main(String[] args) {
QuickPollClient client = new QuickPollClient();
Poll newPoll = new Poll();
newPoll.setQuestion("What is your favourate color?");
Set<Option> options = new HashSet<>();
newPoll.setOptions(options);
Option option1 = new Option(); option1.setValue("Red"); options.add(option1);
Option option2 = new Option(); option2.setValue("Blue");options.add(option2);
URI pollLocation = client.createPoll(newPoll);
System.out.println("Newly Created Poll Location " + pollLocation);
}
PUT Method
The
RestTemplate provides the aptly named
PUT method
to support the PUT HTTP method. Listing
9-8 shows
QuickPollClient’s
updatePoll method that updates a
poll instance. Notice that the
PUT method doesn’t return any response and communicates failures by throwing
RestClientException or its subclasses.
public void updatePoll(Poll poll) {
restTemplate.put(QUICK_POLL_URI_V1 + "/{pollId}", poll, poll.getId());
}
Listing 9-8Update a Poll Using PUT
DELETE Method
The
RestTemplate provides three overloaded
DELETE methods
to support DELETE HTTP operations. The
DELETE methods follow semantics similar to PUT and don’t return a value. They communicate any exceptions via
RestClientException or its subclasses. Listing
9-9 shows the
deletePoll method implementation in
QuickPollClient class.
public void deletePoll(Long pollId) {
restTemplate.delete(QUICK_POLL_URI_V1 + "/{pollId}", pollId);
}
Handling Pagination
In version 2 of our QuickPoll API, we introduced paging. So, the clients upgrading to version 2 need to re-implement the getAllPolls method. All other client methods will remain unchanged.
To re-implement the
getAllPolls
, our first instinct would be to simply pass the
org.springframework.data.domain.PageImpl as the parameterized type reference:
ParameterizedTypeReference<PageImpl<Poll>> responseType = new ParameterizedTypeReference<PageImpl<Poll>>() {};
ResponseEntity<PageImpl<Poll>> responseEntity = restTemplate.exchange(QUICK_POLL_URI_2, HttpMethod.GET, null, responseType);
PageImpl<Poll> allPolls = responseEntity.getBody();
The
PageImpl
is a concrete implementation of the
org.springframework.data.domain.Page interface and can hold all of the paging and sorting information returned by the QuickPoll REST API. The only problem with this approach is that
PageImpl doesn’t have a default constructor and Spring’s HTTP message converter would fail with the following exception:
Could not read JSON: No suitable constructor found for type [simple type, class org.springframework.data.domain.PageImpl<com.apress.domain.Poll>]: can not instantiate from JSON object (need to add/enable type information?)
To handle pagination and successfully map JSON to an object, we will create a Java class that mimics
PageImpl class but also has a default
constructor, as shown in Listing
9-10.
package com.apress.client;
import java.util.List;
import org.springframework.data.domain.Sort;
public class PageWrapper<T> {
private List<T> content;
private Boolean last;
private Boolean first;
private Integer totalPages;
private Integer totalElements;
private Integer size;
private Integer number;
private Integer numberOfElements;
private Sort sort;
// Getters and Setters removed for brevity
}
Listing 9-10PageWrapper Class
The
PageWrapper class can hold the returned content and has attributes to hold the paging information. Listing
9-11 shows the
QuickPollClientV2 class
that makes use of
PageWrapper to interact with second version of API. Notice that the
getAllPolls method now takes two parameters:
page and
size. The
page parameter determines the requested page number, and the
size parameter determines the number of elements to be included in the page. This implementation can be further enhanced to accept sort parameters and provide sorting functionality.
package com.apress.client;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import com.apress.domain.Poll;
public class QuickPollClientV2 {
private static final String QUICK_POLL_URI_2 = "http://localhost:8080/v2/polls";
private RestTemplate restTemplate = new RestTemplate();
public PageWrapper<Poll> getAllPolls(int page, int size) {
ParameterizedTypeReference<PageWrapper<Poll>> responseType = new
ParameterizedTypeReference<PageWrapper<Poll>>() {};
UriComponentsBuilder builder = UriComponentsBuilder
.fromHttpUrl(QUICK_POLL_URI_2)
.queryParam("page", page)
.queryParam("size", size);
ResponseEntity<PageWrapper<Poll>> responseEntity = restTemplate.exchange
(builder.build().toUri(), HttpMethod.GET, null, responseType);
return responseEntity.getBody();
}
}
Listing 9-11QuickPoll Client for Version 2
Handling Basic Authentication
Up to this point we have created clients for the first and second versions of QuickPoll API. In Chapter 8, we secured the third version of the API, and any communication with that version requires Basic authentication. For example, running a DELETE method on URI http://localhost:8080/v3/polls/3 without any authentication would result in an HttpClientErrorException with a 401 status code.
To successfully interact with our QuickPoll v3 API, we need to programmatically base 64 encode a user’s credentials and construct an
authorization request header. Listing
9-12 shows such implementation: we concatenate the passed-in username and password. We then base 64 encode it and create an
Authorization header by prefixing
Basic to the encoded value.
import org.apache.tomcat.util.codec.binary.Base64;
import org.springframework.http.HttpHeaders;
private HttpHeaders getAuthenticationHeader(String username, String password) {
String credentials = username + ":" + password;
byte[] base64CredentialData = Base64.encodeBase64(credentials.getBytes());
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Basic " + new String(base64CredentialData));
return headers;
}
Listing 9-12Authentication Header Implementation
The
RestTemplate’s
exchange method can be used to perform an HTTP operation and takes in an
Authorization header. Listing
9-13 shows the
QuickPollClientV3BasicAuth class with
deletePoll method implementation using Basic authentication.
package com.apress.client;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.web.client.RestTemplate;
import org.springframework.http.HttpEntity;
public class QuickPollClientV3BasicAuth {
private static final String QUICK_POLL_URI_V3 = "http://localhost:8080/v3/polls";
private RestTemplate restTemplate = new RestTemplate();
public void deletePoll(Long pollId) {
HttpHeaders authenticationHeaders = getAuthenticationHeader("admin", "admin");
restTemplate.exchange(QUICK_POLL_URI_V3 + "/{pollId}",
HttpMethod.DELETE, new HttpEntity<Void>(authenticationHeaders), Void.class, pollId);
}
}
Listing 9-13QuickPoll Client with Basic Auth
Testing REST Services
Testing is an important aspect of every software development process. Testing comes in different flavors, and in this chapter, we will focus on unit and integration testing. Unit testing verifies that individual, isolated units of code are working as expected. It is the most common type of testing that developers typically perform. Integration testing typically follows unit testing and focuses on the interaction between previously tested units.
The Java ecosystem is filled with frameworks that ease unit and integration testing. JUnit and TestNG have become the de facto standard test frameworks and provide foundation/integration to most other testing frameworks. Although Spring supports both frameworks, we will be using JUnit in this book, as it is familiar to most readers.
Spring Test
The Spring Framework provides the
spring-test module that allows you to integrate Spring into tests. The module provides a rich set of annotations, utility classes, and mock objects for environment JNDI, Servlet, and Portlet API. The framework also provides capabilities to cache application context across test executions to improve performance. Using this infrastructure, you can easily inject Spring beans and test fixtures into tests. To use the spring-test module in a non–Spring Boot project, you need to include the
Maven dependency as shown in Listing
9-14.
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>3.5.9</version>
<scope>test</scope>
</dependency>
Listing 9-14Spring-test Dependency
Spring Boot provides a starter POM named
spring-boot-starter-test that automatically adds the
spring-test module to a Boot application. Additionally, the starter POM brings in JUnit, Mockito, and Hamcrest
libraries:
Mockito
is a popular mocking framework for Java. It provides a simple and easy-to-use API to create and configure mocks. More details about Mockito can be found at http://mockito.org/.
Hamcrest is a framework that provides a powerful vocabulary for creating matchers. To put it simply, a matcher allows you to match an object against a set of expectations. Matchers improve the way that we write assertions by making them more human readable. They also generate meaningful failure messages when assertions are not met during testing. You can learn more about Hamcrest at http://hamcrest.org/.
To understand the
spring-test module
, let’s examine a typical test case. Listing
9-15 shows a sample test built using
JUnit and
spring-test infrastructure.
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = QuickPollApplication.class)
@WebAppConfiguration
public class ExampleTest {
@Before
public void setup() { }
@Test
public void testSomeThing() {}
@After
public void teardown() { }
}
Listing 9-15Sample JUnit Test
Our example test contains three methods—setup, testSomeThing, and teardown each annotated with a JUnit annotation. The @Test annotation denotes the testSomeThing as a JUnit test method. This method will contain code that ensures our production code works as expected. The @Before annotation
instructs JUnit to run the setup method prior to any test method execution. Methods annotated with @Before can be used for setting up test fixtures and test data. Similarly, the @After annotation instructs JUnit to run the teardown method after any test method execution. Methods annotated with @After
are typically used to tear down test fixtures and perform cleanup operations.
JUnit
uses the notion of test runner to perform test execution. By default, JUnit uses the BlockJUnit4ClassRunner test runner to execute test methods and associated life cycle (@Before or @After, etc.) methods. The @RunWith annotation allows you to alter this behavior. In our example, using the @RunWith annotation, we are instructing JUnit to use the SpringJUnit4ClassRunner class to run the test cases.
The SpringJUnit4ClassRunner
adds Spring integration by performing activities such as loading application context, injecting autowired dependencies, and running specified test execution listeners. For Spring to load and configure an application context, it needs the locations of the XML context files or the names of the Java configuration classes. We typically use the @ContextConfiguration annotation to provide this information to the SpringJUnit4ClassRunner class.
In our example, however, we use the SpringBootTest, a specialized version of the standard ContextConfiguration that provides additional Spring Boot features. Finally, the @WebAppConfiguration annotation instructs Spring to create the web version of the application context, namely, WebApplicationContext.
Unit Testing REST Controllers
Spring’s dependency injection makes
unit testing easier. Dependencies can be easily mocked or simulated with predefined behavior, thereby allowing us to zoom in and test code in isolation. Traditionally, unit testing Spring MVC controllers followed this paradigm. For example, Listing
9-16 shows the code unit testing
PollController’s
getAllPolls method.
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import java.util.ArrayList;
import com.google.common.collect.Lists;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.util.ReflectionTestUtils;
public class PollControllerTestMock {
@Mock
private PollRepository pollRepository;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
}
@Test
public void testGetAllPolls() {
PollController pollController = new PollController();
ReflectionTestUtils.setField(pollController, "pollRepository", pollRepository);
when(pollRepository.findAll()).thenReturn(new ArrayList<Poll>());
ResponseEntity<Iterable<Poll>> allPollsEntity = pollController.getAllPolls();
verify(pollRepository, times(1)).findAll();
assertEquals(HttpStatus.OK, allPollsEntity.getStatusCode());
assertEquals(0, Lists.newArrayList(allPollsEntity.getBody()).size());
}
}
Listing 9-16Unit Testing PollController with Mocks
The PollControllerTestMock implementation uses the Mockito’s @Mock annotation
to mock PollController’s only dependency: PollRepository. For Mockito to properly initialize the annotated pollRepository property, we either need to run the test using the MockitoJUnitRunner test runner or invoke the initMocks method in the MockitoAnnotations. In our test, we choose the latter approach and call the initMocks in the @Before method.
In the testGetAllPolls method
, we create an instance of PollController and inject the mock PollRepository using Spring’s ReflectionTestUtils utility class. Then we use Mockito’s when and thenReturn methods to set the PollRepository mock’s behavior. Here are we indicating that when the PollRepository’s findAll() method
is invoked, an empty collection should be returned. Finally, we invoke the getAllPolls method and verify findAll() method’s invocation and assert controller’s return value.
In this strategy, we treat the PollController as a POJO and hence don’t test the controller’s request mappings, validations, data bindings, and any associated exception handlers. Starting from version 3.2, spring-test module includes a Spring MVC Test framework that allows us to test a controller as a controller. This test framework will load the DispatcherServlet
and associated web components such as controllers and view resolvers into test context. It then uses the DispatcherServlet to process all the requests and generates responses as if it is running in a web container without actually starting up a web server. This allows us to perform a more thorough testing of Spring MVC applications.
Spring MVC Test Framework Basics
To gain a better understanding of the Spring MVC Test framework, we explore its four important classes: MockMvc, MockMvcRequestBuilders, MockMvcResultMatchers, and MockMvcBuilders. As evident from the class names, the Spring MVC Test framework makes heavy use of Builder pattern.2
Central to the test framework is the
org.springframework.test.web.servlet.MockMvc class
, which can be used to perform HTTP requests. It contains only one method named
perform and has the following API signature:
public ResultActions perform(RequestBuilder requestBuilder) throws java.lang.Exception
The
RequestBuilder parameter
provides an abstraction to create the request (GET, POST, etc.) to be executed. To simplify request construction, the framework provides an
org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder implementation and a set of helper static methods in the
org.springframework.test.web.servlet.request.MockMvcRequestBuilders class. Listing
9-17 gives an example of a
POST HTTP request constructed using the previously mentioned classes.
post("/test_uri")
.param("admin", "false")
.accept(MediaType.APPLICATION_JSON)
.content("{JSON_DATA}");
Listing 9-17POST HTTP Request
The post method
is part of the MockMvcRequestBuilders class and is used to create a POST request. The MockMvcRequestBuilders also provides additional methods such as get, delete, and put to create corresponding HTTP requests. The param method is part of the MockHttpServletRequestBuilder class and is used to add a parameter to the request. The MockHttpServletRequestBuilder provides additional methods such as accept, content, cookie, and header to add data and metadata to the request being constructed.
The
perform method
returns an
org.springframework.test.web.servlet.ResultActions instance that can be used to apply assertions/expectations on the executed response. Listing
9-18 shows three assertions applied to the response of a sample POST request using
ResultActions’s
andExpect method. The
status is a static method in
org.springframework.test.web.servlet.result.MockMvcResultMatchers that allows you to apply assertions on response status. Its
isOk method asserts that the status code is 200 (HTTPStatus.OK). Similarly, the
content method
in
MockMvcResultMatchers provides methods to assert response body. Here we are asserting that the response content type is of type “application/json” and matches an expected string “
JSON_DATA.”
mockMvc.perform(post("/test_uri"))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(content().string("{JSON_DATA}"));
Listing 9-18ResultActions
So far, we have looked at using
MockMvc to perform requests and assert the response. Before we can use
MockMvc, we need to initialize it. The
MockMvcBuilders class
provides the following two methods to build a
MockMvc instance:
WebAppContextSetup—Builds a MockMvc instance using a fully initialized WebApplicationContext. The entire Spring configuration associated with the context is loaded before MockMvc instance is created. This technique is used for end-to-end testing.
StandaloneSetup—Builds a MockMvc without loading any Spring configuration. Only the basic MVC infrastructure is loaded for testing controllers. This technique is used for unit testing.
Unit Testing Using Spring MVC Test Framework
Now that we have reviewed the Spring MVC Test framework, let’s look at using it to test REST controllers. The
PollControllerTest class in Listing
9-19 demonstrates testing the
getPolls method
. To the
@ContextConfiguration annotation, we pass in a
MockServletContext class instructing Spring to set up an empty
WebApplicationContext. An empty
WebApplicationContext allows us to instantiate and initialize the one controller that we want to test without loading up the entire application context. It also allows us to mock the dependencies that the controller requires.
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.mock.web.MockServletContext;
import org.springframework.test.web.servlet.MockMvc;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = QuickPollApplication.class)
@ContextConfiguration(classes = MockServletContext.class)
@WebAppConfiguration
public class PollControllerTest {
@InjectMocks
PollController pollController;
@Mock
private PollRepository pollRepository;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
mockMvc = standaloneSetup(pollController).build();
}
@Test
public void testGetAllPolls() throws Exception {
when(pollRepository.findAll()).thenReturn(new ArrayList<Poll>());
mockMvc.perform(get("/v1/polls"))
.andExpect(status().isOk())
.andExpect(content().string("[]"));
}
}
Listing 9-19Unit Testing with Spring MVC Test
In this case, we want to test version 1 of our PollController API. So we declare a pollController property and annotate it with @InjectMocks. During runtime, Mockito sees the @InjectMocks annotation and will create an instance of the import com.apress.v1.controller.PollController.PollController. It then injects it with any mocks declared in the PollControllerTest class
using constructor/field or setter injection. The only mock we have in the class is the PollRepository.
In the @Before annotated method, we use the MockMvcBuilders’s standaloneSetup() method to register the pollController instance. The standaloneSetup() automatically creates the minimum infrastructure required by the DispatcherServlet to serve requests associated with the registered controllers. The MockMvc instance built by standaloneSetup is stored in a class-level variable and made available to tests.
In the testGetAllPolls method
, we use Mockito to program the PollRepository mock’s behavior. Then we perform a GET request on the /v1/polls URI and use the status and content assertions to ensure that an empty JSON array is returned. This is the biggest difference from the version that we saw in Listing 9-16. There we were testing the result of a Java method invocation. Here we are testing the HTTP response that the API generates.
Integration Testing REST Controllers
In the previous section, we looked at unit testing a controller and its associated configuration. However, this testing is limited to a web layer. There are times when we want to test all of the layers of an application from controllers to the persistent store. In the past, writing such tests required launching the application in an embedded Tomcat or Jetty server and use a framework such as HtmlUnit or RestTemplate to trigger HTTP requests. Depending on an external servlet container can be cumbersome and often slows down testing.
The Spring MVC Test framework provides a lightweight, out-of-the-container alternative for integration testing MVC applications. In this approach, the entire Spring application context along with the DispatcherServlet and associated MVC infrastructure gets loaded. A mocked MVC container is made available to receive and execute HTTP requests. We interact with real controllers and these controllers work with real collaborators. To speed up integration testing, complex services are sometimes mocked. Additionally, the context is usually configured such that the DAO/repository layer interacts with an in-memory database.
This approach is similar to the approach we used for unit testing controllers, except for these three
differences:
The entire Spring context gets loaded as opposed to an empty context in the unit testing case.
All REST endpoints are available as opposed to the ones configured via standaloneSetup.
Tests are performed using real collaborators against in-memory database as opposed to mocking dependency’s behavior.
An integration test for the
PollController’s
getAllPolls method
is shown in Listing
9-20. The
PollControllerIT class is similar to the
PollControllerTest that we looked at earlier. A fully configured instance of
WebApplicationContext is injected into the test. In the
@Before method, we use this
WebApplicationContext instance to build a
MockMvc instance using
MockMvcBuilders’s
webAppContextSetup.
import static org.hamcrest.Matchers.hasSize;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.web.context.WebApplicationContext;
import com.apress.QuickPollApplication;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = QuickPollApplication.class)
@WebAppConfiguration
public class PollControllerIT {
@Inject
private WebApplicationContext webApplicationContext;
@Before
public void setup() {
mockMvc = webAppContextSetup(webApplicationContext).build();
}
@Test
public void testGetAllPolls() throws Exception {
mockMvc.perform(get("/v1/polls"))
.andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(20)));
}
}
Listing 9-20Integration Testing with Spring MVC Test
The
testGetAllPolls method implementation uses the
MockMvc instance to perform a GET request on the
/v1/polls endpoint. We use two
assertions to ensure that the result is what we expect:
The isOK assertion ensures that we get a status code 200.
The JsonPath method allows us to write assertions against response body using JsonPath expression. The JsonPath (http://goessner.net/articles/JsonPath/) provides a convenient way to extract parts of a JSON document. To put it simply, JsonPath is to JSON is what XPath is to XML.
In our test case, we use the Hamcrest’s hasSize matcher
to assert that the retuned JSON contains 20 polls. The import.sql script used to populate the in-memory database contains 20 poll entries. Hence, our assertion uses the magic number 20 for comparison.
Summary
Spring provides powerful template and utility classes that simplify REST client development. In this chapter, we reviewed RestTemplate and used it to perform client operations such as GET, POST, PUT, and DELETE on resources. We also reviewed the Spring MVC Test framework and its core classes. Finally, we used the test framework to simplify unit and integration test creation.