DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workkloads.

Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • Supercharging Productivity in Microservice Development With AI Tools
  • REST APIs Test Pyramid
  • Practical Use of Weak Symbols
  • Building a Distributed Multi-Language Data Science System

Trending

  • How to Submit a Post to DZone
  • DZone's Article Submission Guidelines
  • Docker Base Images Demystified: A Practical Guide
  • How Large Tech Companies Architect Resilient Systems for Millions of Users
  1. DZone
  2. Testing, Deployment, and Maintenance
  3. Testing, Tools, and Frameworks
  4. REST Endpoint Testing With MockMvc

REST Endpoint Testing With MockMvc

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

By 
Brian Hannaway user avatar
Brian Hannaway
·
Updated Jun. 03, 19 · Tutorial
Likes (6)
Comment
Save
Tweet
Share
175.2K Views

Join the DZone community and get the full member experience.

Join For Free

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.

REST unit test

Published at DZone with permission of Brian Hannaway, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Supercharging Productivity in Microservice Development With AI Tools
  • REST APIs Test Pyramid
  • Practical Use of Weak Symbols
  • Building a Distributed Multi-Language Data Science System

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!