Spring WebMVC Test

Spring%20WebMVC%20Test%20Docs 2088E9?logo=quickLook&logoColor Spring%20Boot%20WebMVC%20Test%20Docs 2088E9?logo=quickLook&logoColor

Overview

Spring MVC Controllers are tricky to test properly. They have a high degree of integration with Spring MVC framework. JUnit tests are not sufficient to test the framework interaction.

Spring Boot supports a concept of Test Splices which bring up a targeted segment of the autoconfigured Spring Boot environment:

  • e.g. just the Database components or just the Web components,

  • user defined Spring beans typically are not initialized.

WebMvcTest

@WebMvcTest

A Spring Boot test splice which creates a MockMVC environment for the Controller under a test. Dependencies of it are not included and need to be added to the Spring Context in the test environment.

@WebMvcTest will autoconfigure all the Controllers. If you want to autoconfigure only specific Controllers, use it like: @WebMvcTest(BookController.class)
🔴 MockMvc

Main entry point for server-side Spring MVC test support.

In @WebMvcTest you can autowire 🔴 MockMvc
@Autowired
MockMvc mockMvc;

Testing Controller requests

🟠 MockMvcRequestBuilders

Static factory methods for building 🟢 MockHttpServletRequest which is mock implementation of the ⚪ HttpServletRequest.

🟠 MockMvcResultMatchers

Static factory methods for building ⚪ ResultMatcher-based result actions. A ⚪ ResultMatcher matches the result of an executed request against some expectation.

Test Controller GET request
mockMvc.perform(MockMvcRequestBuilders.get("/api/v1/book/{bookId}", testBookDTO.getId())
        .accept(MediaType.APPLICATION_JSON))
    .andExpect(MockMvcResultMatchers.status().isOk())
    .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON));
Test Controller POST request
mockMvc.perform(MockMvcRequestBuilders.post("/api/v1/book")
        .accept(MediaType.APPLICATION_JSON)
        .contentType(MediaType.APPLICATION_JSON)
        .content(objectMapper.writeValueAsString(testBookDTO))) (1)
    .andExpect(MockMvcResultMatchers.status().isCreated())
    .andExpect(MockMvcResultMatchers.header().exists(HttpHeaders.LOCATION));
1 Converts testBookDTO into JSON with FasterXML Jackson
Test Controller PUT request
mockMvc.perform(MockMvcRequestBuilders.put("/api/v1/book/{bookId}", testBookDTO.getId())
        .accept(MediaType.APPLICATION_JSON)
        .contentType(MediaType.APPLICATION_JSON)
        .content(objectMapper.writeValueAsString(testBookDTO)))
    .andExpect(MockMvcResultMatchers.status().isNoContent());
Test Controller PATCH request
Map<String, Object> bookMap = new HashMap<>();
bookMap.put("name", "New Book Name");
mockMvc.perform(MockMvcRequestBuilders.patch("/api/v1/book/{bookId}", testBookDTO.getId())
        .accept(MediaType.APPLICATION_JSON)
        .contentType(MediaType.APPLICATION_JSON)
        .content(objectMapper.writeValueAsString(bookMap)))
    .andExpect(MockMvcResultMatchers.status().isNoContent());
Test Controller DELETE request
mockMvc.perform(MockMvcRequestBuilders.delete("/api/v1/book/{bookId}", testBookDTO.getId())
        .accept(MediaType.APPLICATION_JSON)
    .andExpect(MockMvcResultMatchers.status().isNoContent());

Mocking Beans

@MockitoBean

Can be used in test classes to override a bean in the test’s ⚪ ApplicationContext with a Mockito mock. For example a Service which is called by Controller.

Return an object on given method call
@MockitoBean
BookService bookService;

@Test
void testGetBookById() throws Exception {
    BookDTO testBookDTO = // ...
    BDDMockito.given(bookService.getBookById(ArgumentMatchers.any(UUID.class))) (1) (2)
        .willReturn(testBookDTO);
    // ...
}
1 🟢 BDDMockito - Behavior Driven Development style of writing tests uses //given //when //then comments as fundamental parts of your test methods.
2 🟢 ArgumentMatchers - allows flexible verification or stubbing. For example any() matches anything, including nulls, while any(Class<T> type) matches any object of given type, excluding nulls.
Verify that behavior happened once
Mockito.verify(bookService).updateBookById(ArgumentMatchers.any(UUID.class), ArgumentMatchers.any(BookDTO.class)); (1)
1 🟢 Mockito#verify(T mock) - verifies certain behavior happened once. It is alias to: verify(mock, Mockito.times(1)).someMethod("some arg");.
Capture and assert arguments
@Captor (1)
ArgumentCaptor<UUID> uuidArgumentCaptor;

@Test
void testDeleteBookById() throws Exception {
    BookDTO testBookDTO = // ...
    // ...
    Mockito.verify(bookService).deleteBookById(uuidArgumentCaptor.capture());
    Assertions.assertThat(testBookDTO.getId()).isEqualTo(uuidArgumentCaptor.getValue()); (2)
}
1 @Captor - allows shorthand 🟢 ArgumentCaptor creation on fields (instead of ArgumentCaptor<UUID> uuidArgumentCaptor = ArgumentCaptor.forClass(UUID.class)).
2 🟢 Assertions - AssertJ entry point for assertion methods for different types. Each method in this class is a static factory for a type-specific assertion object.
Throw an exception on given method call
BDDMockito.given(bookService.getBookById(ArgumentMatchers.any(UUID.class)))
        .willThrow(NotFoundException.class);

Jayway JsonPath

Jayway%20JsonPath%20Docs 2088E9?logo=quickLook&logoColor=white

A Java DSL (Domain Specific Language) for reading JSON documents. It is included in Spring Boot Test dependency. Useful for performing assertions against the JSON object that is coming back from 🔴 MockMvc.

JsonPath expressions can use the dot-notation:
$.store.book[0].title
or the bracket-notation:
$['store']['book'][0]['title']
Example of 🔴 MockMvc json path validation
.andExpect(MockMvcResultMatchers.jsonPath("$.content[1].id", Is.is("some-id-123"))) (1)
.andExpect(MockMvcResultMatchers.jsonPath("$.content.length()", Is.is(3))); (2)
1 🟢 Is#is(T value) - matcher from JavaHamcrest which is a shortcut to the frequently used is(equalTo(x)) (where is(Matcher<T> matcher) only decorates another matcher).
2 Asserts that we have 3 objects in content array.

Testing Exception Handling

Test 404 HTTP Response Code
mockMvc.perform(MockMvcRequestBuilders.get("/api/v1/book/{bookId}", UUID.randomUUID()))
    .andExpect(status().isNotFound());