Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

REST Endpoint Testing With MockMvc

DZone 's Guide to

REST Endpoint Testing With MockMvc

See how to test a Spring REST endpoint without a servlet container.

· Integration Zone ·
Free Resource

In this post, I'm going to show you how to test a Spring REST endpoint without a servlet container. In the past, integration tests were the only meaningful way to test a Spring REST endpoint. This involved spinning up a container like Tomcat or Jetty, deploying the application, calling the endpoint, running some assertions, and then stopping the container. While this is an effective way to test an endpoint, it isn't particularly fast. We're forced to wait while the entire application is stood up, just to test a single endpoint.

An alternative approach is to write vanilla unit tests for each REST controller, manually instantiating the Controller and mocking out any dependencies. These tests will run much faster than integration tests, but they're of limited value. The problem is, by manually creating the Controller outside of the Spring Application Context, the controller loses all the useful request/response handling that Spring takes care of on our behalf. Things like:

  • request routing/URL mapping
  • request deserialization
  • response serialization
  • exception translation

What we'd really like is the best of both worlds. The ability to test a fully functional REST controller but without the overhead of deploying the app to a container. Thankfully, that's exactly what MockMvc allows you to do. It stands up a Dispatcher Servlet and all required MVC components, allowing you to test an endpoint in a proper web environment, but without the overhead of running a container.

Defining a Controller

Before we can put MockMvc through its paces, we need a REST endpoint to test. The controller below exposes 2 endpoints, one to create an Account and one to retrieve an Account.

@RestController
public class AccountController {
  private AccountService accountService;

  @Autowired
  public AccountController(AccountService accountService) {
    this.accountService = accountService;
  }

  @RequestMapping(value = { "/api/account" }, method = { RequestMethod.POST })
  public Account createAccount(@RequestBody Account account, 
                               HttpServletResponse httpResponse, 
                               WebRequest request) {

    Long accountId = accountService.createAccount(account);
    account.setAccountId(accountId);

    httpResponse.setStatus(HttpStatus.CREATED.value());
    httpResponse.setHeader("Location", String.format("%s/api/account/%s", 
                            request.getContextPath(), accountId)); 
    return account;
  }

  @RequestMapping(value = "/api/account/{accountId}", method = RequestMethod.GET)
  public Account getAccount(@PathVariable("accountId") Long accountId) {

    /* validate account Id parameter */
    if (accountId < 9999) {
      throw new InvalidAccountRequestException();
    }

    Account account = accountService.loadAccount(accountId);
    if(null==account){ 
      throw new AccountNotFoundException();
    }

    return account;
  }

}
  • createAccount — calls the AccountService to create the Account, then returns the Account along with a HTTP header specifying its location for future retrieval. Later, we'll mock the AccountService with Mockito so that we can keep our tests focused on the REST layer.
  • retrieveAccount — takes an account Id from the URL and performs some simple validation to ensure the value is greater than 9999. If the validation fails a custom  InvalidAccountRequestException  is thrown. This exception is caught and translated by an exception handler (defined later). Next, the mock  AccountService is called to retrieve the Account, before returning it to the client.

Exception Handler

The custom runtime exceptions thrown in getAccount are intercepted and mapped to appropriate HTTP response codes using the exception handler defined below.

@Slf4j
@ControllerAdvice
public class ControllerExceptionHandler {

  @ResponseStatus(HttpStatus.NOT_FOUND) // 404
  @ExceptionHandler(AccountNotFoundException.class)
  public void handleNotFound(AccountNotFoundException ex) {
    log.error("Requested account not found");
  }

  @ResponseStatus(HttpStatus.BAD_REQUEST) // 400
  @ExceptionHandler(InvalidAccountRequestException.class)
  public void handleBadRequest(InvalidAccountRequestException ex) {
    log.error("Invalid account supplied in request");
  }

  @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) // 500
  @ExceptionHandler(Exception.class)
  public void handleGeneralError(Exception ex) {
    log.error("An error occurred processing request" + ex);
  }
}

MockMVC Setup

The @SpringBootTest annotation is used to specify the application configuration to load before running the tests. We could have referenced a test specific configuration here, but given our simple project setup its fine to use the main Application config class.

import static org.mockito.Matchers.any;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
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;

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment=WebEnvironment.MOCK, classes={ Application.class })
public class AccountControllerTest {

  private MockMvc mockMvc;

  @Autowired
  private WebApplicationContext webApplicationContext;

  @MockBean 
  private AccountService accountServiceMock;

  @Before
  public void setUp() {
    this.mockMvc = webAppContextSetup(webApplicationContext).build();
  }

The injected WebApplicationContext is a sub-component of Springs main application context and encapsulates configuration for Spring web components such as the controller and exception handler we defined earlier.

The @MockBean annotation tells Spring to create a mock instance of AccountService and add it to the application context so that it's injected into AccountController. We have a handle on it in the test so that we can define its behavior before running each test.

The setup method uses the statically imported webAppContextSetup method from  MockMvcBuilders  and the injected WebApplicationContext to build a MockMvc instance.

Create Account Test

Lets put the Account MockMvc instance to work with a test for the create account endpoint.

@Test
public void should_CreateAccount_When_ValidRequest() throws Exception {

  when(accountServiceMock.createAccount(any(Account.class))).thenReturn(12345L);

  mockMvc.perform(post("/api/account")
           .contentType(MediaType.APPLICATION_JSON)
           .content("{ "accountType": "SAVINGS", "balance": 5000.0 }") 
           .accept(MediaType.APPLICATION_JSON))
           .andExpect(status().isCreated())
           .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
           .andExpect(header().string("Location", "/api/account/12345"))
           .andExpect(jsonPath("$.accountId").value("12345")) 
           .andExpect(jsonPath("$.accountType").value("SAVINGS"))
           .andExpect(jsonPath("$.balance").value(5000)); 
}

On line 4, we use Mockito to define the expected behavior of the mock AccountService. We tell the mock that when it receives an Account it should return 12345.

Lines 6 to 9 use mockMvc to define a POST request to /api/account. The request content type is JSON and the request body contains a JSON definition of the account to be created. Finally, an accept header is set to tell the endpoint the client expects a JSON response.

Lines 10 to 15 use statically imported methods from MockMvcResukltMatchers to perform assertions on the response. We begin by checking that the response code returned is 201 'Created' and that the content type is JSON. We then check for the existence of the HTTP header 'Location' that contains the request URL for retrieving the created account. The final 3 lines use jsonPath to check that the JSON response is in the format expected. JsonPath is a JSON equivalent to XPath that allows you to query JSON using path expressions. For more information take a look at their documentation.

Retrieve Account Test

The retrieve account test follows a similar pattern to test we described above.

@Test
public void should_GetAccount_When_ValidRequest() throws Exception {

  /* setup mock */
  Account account = new Account(12345L, EnumAccountType.SAVINGS, 5000.0);
  when(accountServiceMock.loadAccount(12345L)).thenReturn(account);

  mockMvc.perform(get("/api/account/12345") 
           .accept(MediaType.APPLICATION_JSON))
           .andExpect(status().isOk())
           .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
           .andExpect(jsonPath("$.accountId").value(12345))
           .andExpect(jsonPath("$.accountType").value("SAVINGS"))
           .andExpect(jsonPath("$.balance").value(5000.0));
}

We begin by creating an Account and use it to define the behavior of the mock AccountService. The MockMvc instance is used to perform GET request that expects a JSON response. We check the response for a 200 'OK' response code, a JSON content type and a JSON response body containing the requested account.

Retrieve Account Error Test

@Test
public void should_Return404_When_AccountNotFound() throws Exception {

  /* setup mock */
  when(accountServiceMock.loadAccount(12345L)).thenReturn(null);

  mockMvc.perform(get("/api/account/12345") 
          .accept(MediaType.APPLICATION_JSON))
          .andExpect(status().isNotFound());
}

Finally, we'll test an error scenario. The mock AccountService will return null causing an  AccountNotFoundException to be thrown. This allows us to test ControllerExceptionHandler we defined earlier.  Line 9 above expects the AccountNotFoundExcepion to result in a 404 response to the client.

Wrapping Up

In this post, we looked at how MockMvc can help you test your REST endpoints without standing up a servlet container.  MockMvc hits the sweet spot between slow integration tests and fast (relatively low-value) unit tests. You get the benefit of testing your fully functional REST layer without the overhead of deploying to a container.

The sample code for this post is available on GitHub so feel free to pull it and have a play around. If you have any comments or questions please leave a note below.

Topics:
spring ,rest ,spring rest ,mockmvc ,rest testing ,tutorial ,integration

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}