The previous part of my Spring MVC Test tutorial described how we can send HTTP requests to the system under test and write assertions for the response returned by the tested controller method. This blog post describes how we can use the information provided by the previous part of this tutorial when we are writing unit tests for a controller method which renders the information of a single item.
After we have finished this blog post, we:
- Know how we can ensure that the system under test returns the correct HTTP status code.
- Can verify that the system under test renders the correct view.
- Understand how we can ensure that our model attributes contain the correct information.
Let's begin.
Introduction to the System Under Test
We have to write unit tests for a controller method which processes GET
requests send to the path: '/todo-item/{id}'. This method returns the HTTP status code 200 and renders the information of a todo item whose id is given as the value of the id
path variable. If the requested todo item isn't found from the database, this method returns the HTTP status code 404 and renders the not found view.
The tested controller method is called findById()
and it's implemented by following these steps:
- Find the todo item from the database by invoking the
findById()
method of theTodoItemCrudService
class. Pass the id of the todo item to the invoked method as an argument. - Put the found todo item to a model attribute called
todoItem
. - Return the name of the view ('todo-item/view') that renders the information of the found todo item.
The source code of the tested controller method looks as follows:
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import java.util.List; @Controller @RequestMapping("/todo-item") public class TodoItemCrudController { private final TodoItemCrudService service; @Autowired public TodoItemCrudController(TodoItemCrudService service) { this.service = service; } @GetMapping("{id}") public String findById(@PathVariable("id") Long id, Model model) { TodoItemDTO found = service.findById(id); model.addAttribute("todoItem", found); return "todo-item/view"; } }
The TodoItemDTO
class is a DTO that contains the information of a single todo item. Its source code looks as follows:
public class TodoItemDTO { private Long id; private String description; private List<TagDTO> tags; private String title; private TodoItemStatus status; //Getters and setters are omitted }
The TagDTO
class is a a DTO that contains the information of a single tag. Its source code looks as follows:
public class TagDTO { private Long id; private String name; //Getters and setters are omitted }
The TodoItemStatus
enum specifies the possible statuses of a todo item. Its source code looks as follows:
public enum TodoItemStatus { OPEN, IN_PROGRESS, DONE }
Next, we will learn how we can write assertions for the response returned by the system under test.
Writing Assertions for the Response Returned by the System Under Test
Before we can write unit tests for a Spring MVC controller which renders data, we have to learn how we can write assertions for the response returned by the system under test. When we want to write assertions for the response returned by the tested Spring MVC controller, we have to use these static
methods of the MockMvcResultMatchers
class:
- The
status()
method returns aStatusResultMatchers
object which allows us to write assertions for the returned HTTP status. - The
view()
method returns aViewResultMatchers
object which allows us to write assertions for the rendered view. - The
model()
method returns aModelResultMatchers
object which allows us to write assertions for the Spring MVC model.
Let's move on and find out how we can write a request builder method which sends GET
requests to the system under test.
Writing a New Request Builder Method
Because we want to remove duplicate code from our test class, we have to create and send HTTP requests to the system under test by using a so called request builder class. In other words, before we can write unit tests for the system under test, we have write to a request builder method which creates and sends HTTP requests to the system under test. We can write this request builder method by following these steps:
- Add a new method called
findById()
to our request builder class. Ensure that this method takes the id of the todo item as a method parameter and returns aResultActions
object. - Send a
GET
request to the path: '/todo-item/{id}' by invoking theperform()
method of theMockMvc
class. Remember to return theResultActions
object that's returned by theperform()
method.
After we have written our request builder method, the source code of our request builder class looks as follows:
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; class TodoItemRequestBuilder { private final MockMvc mockMvc; TodoItemRequestBuilder(MockMvc mockMvc) { this.mockMvc = mockMvc; } ResultActions findById(Long id) throws Exception { return mockMvc.perform(get("/todo-item/{id}", id)); } }
Next, we will learn to write unit tests for the system under test.
Writing Unit Tests for the System Under Test
When we want to write unit tests for the system under test, we have to follow these steps:
First, we have to add the required class hierarchy to our test class. Because we are writing unit tests, we can create this class hierarchy by following these steps:
- Add an inner class called
FindById
to our test class. This inner class contains the test methods which ensure that the system under test is working as expected. - Add an inner class called
WhenRequestedTodoItemIsNotFound
to theFindById
class. This inner class contains the test methods which ensure that the system under test is working as expected when the requested todo item isn't found from the database. - Add an inner class called
WhenRequestedTodoItemIsFound
to theFindById
class. This inner class contains the test methods which ensure that the system under test is working as expected when the requested todo item is found from the database.
After we have created the required class hierarchy, the source code of our test class looks as follows:
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import static net.petrikainulainen.springmvctest.junit5.web.WebTestConfig.*; import static org.mockito.Mockito.mock; class TodoItemCrudControllerTest { private TodoItemRequestBuilder requestBuilder; private TodoItemCrudService service; @BeforeEach void configureSystemUnderTest() { service = mock(TodoItemCrudService.class); MockMvc mockMvc = MockMvcBuilders .standaloneSetup(new TodoItemCrudController(service)) .setHandlerExceptionResolvers(exceptionResolver()) .setLocaleResolver(fixedLocaleResolver()) .setViewResolvers(jspViewResolver()) .build(); requestBuilder = new TodoItemRequestBuilder(mockMvc); } @Nested @DisplayName("Render the the information of the requested todo item") class FindById { @Nested @DisplayName("When the requested todo item isn't found from the database") class WhenRequestedTodoItemIsNotFound { } @Nested @DisplayName("When the requested todo item is found from the database") class WhenRequestedTodoItemIsFound { } } }
Second, we have to ensure that the system under test is working as expected when the requested todo item isn't found from the database. We can write the required test methods by following these steps:
- Add a constant called
TODO_ITEM_ID
to theFindById
class. This constant specifies the id of the requested todo item. We have to add this constant to theFindById
class because its value is used by test methods found from theWhenRequestedTodoItemIsNotFound
andWhenRequestedTodoItemIsFound
classes. - Add a new setup method to the
WhenRequestedTodoItemIsNotFound
class and ensure that it's run before a test method is run. When we implement this setup method, we must ensure that theTodoItemCrudService
object throws aTodoItemNotFoundException
when itsfindById()
method is invoked by using the argument:99L
. - Ensure that the system under test returns the HTTP status code 404.
- Verify that the system under test renders the not found view.
After we have written the required test methods, the source code of our test class looks as follows:
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import static net.petrikainulainen.springmvctest.junit5.web.WebTestConfig.*; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view; class TodoItemCrudControllerTest { private TodoItemRequestBuilder requestBuilder; private TodoItemCrudService service; @BeforeEach void configureSystemUnderTest() { service = mock(TodoItemCrudService.class); MockMvc mockMvc = MockMvcBuilders .standaloneSetup(new TodoItemCrudController(service)) .setHandlerExceptionResolvers(exceptionResolver()) .setLocaleResolver(fixedLocaleResolver()) .setViewResolvers(jspViewResolver()) .build(); requestBuilder = new TodoItemRequestBuilder(mockMvc); } @Nested @DisplayName("Render the information of the requested todo item") class FindById { private final Long TODO_ITEM_ID = 99L; @Nested @DisplayName("When the requested todo item isn't found from the database") class WhenRequestedTodoItemIsNotFound { @BeforeEach void serviceThrowsNotFoundException() { given(service.findById(TODO_ITEM_ID)) .willThrow(new TodoItemNotFoundException("")); } @Test @DisplayName("Should return the HTTP status code 404") void shouldReturnHttpStatusCodeNotFound() throws Exception { requestBuilder.findById(TODO_ITEM_ID) .andExpect(status().isNotFound()); } @Test @DisplayName("Should render the 404 view") void shouldRender404View() throws Exception { requestBuilder.findById(TODO_ITEM_ID) .andExpect(view().name("error/404")); } } //The other inner class is omitted } }
Third, we have to ensure that the system under test is working as expected when the requested todo item is found from the database. We can write the required test methods by following these steps:
- Add the required constants to the
WhenRequestedTodoItemIsFound
class. These constants specify the property values of the found todo item. - Add a new setup method to the
WhenRequestedTodoItemIsFound
class and ensure that it's run before a test method is run. When we implement this setup method, we must ensure that theTodoItemCrudService
object returns the information of the found todo item when itsfindById()
method is invoked by using the argument:99L
. - Ensure that the system under test returns the HTTP status code 200.
- Verify that the system under test renders the view which displays the information of the found todo item.
- Ensure that the system under test displays the information of the correct todo item.
- Verify that the system under test displays the correct title and description.
- Ensure that the system under test displays an open todo item.
- Verify that the system under test displays a todo item that has one tag.
- Ensure that the system under test displays the information of the found tag.
After we have written the required test methods, the source code of our test class looks as follows:
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import java.util.Collections; import static net.petrikainulainen.springmvctest.junit5.web.WebTestConfig.*; import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.hasProperty; import static org.hamcrest.Matchers.hasSize; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view; class TodoItemCrudControllerTest { private TodoItemRequestBuilder requestBuilder; private TodoItemCrudService service; @BeforeEach void configureSystemUnderTest() { service = mock(TodoItemCrudService.class); MockMvc mockMvc = MockMvcBuilders .standaloneSetup(new TodoItemCrudController(service)) .setHandlerExceptionResolvers(exceptionResolver()) .setLocaleResolver(fixedLocaleResolver()) .setViewResolvers(jspViewResolver()) .build(); requestBuilder = new TodoItemRequestBuilder(mockMvc); } @Nested @DisplayName("Render the information of the requested todo item") class FindById { private final Long TODO_ITEM_ID = 99L; //The other inner class is omitted @Nested @DisplayName("When the requested todo item is found from the database") class WhenRequestedTodoItemIsFound { private final String TITLE = "Write example project"; private final String DESCRIPTION = "Use JUnit 5"; private final TodoItemStatus STATUS_OPEN = TodoItemStatus.OPEN; private final Long TAG_ID = 44L; private final String TAG_NAME = "tag"; @BeforeEach void serviceReturnsOpenTodoItemWithOneTag() { TodoItemDTO found = new TodoItemDTO(); found.setId(TODO_ITEM_ID); found.setTitle(TITLE); found.setDescription(DESCRIPTION); found.setStatus(STATUS_OPEN); TagDTO tag = new TagDTO(); tag.setId(TAG_ID); tag.setName(TAG_NAME); found.setTags(Collections.singletonList(tag)); given(service.findById(TODO_ITEM_ID)).willReturn(found); } @Test @DisplayName("Should return the HTTP status code 200") void shouldReturnHttpStatusCodeOk() throws Exception { requestBuilder.findById(TODO_ITEM_ID) .andExpect(status().isOk()); } @Test @DisplayName("Should render the view todo item view") void shouldRenderViewTodoItemView() throws Exception { requestBuilder.findById(TODO_ITEM_ID) .andExpect(view().name("todo-item/view")); } @Test @DisplayName("Should display the information of the correct todo item") void shouldDisplayInformationOfCorrectTodoItem() throws Exception { requestBuilder.findById(TODO_ITEM_ID) .andExpect(model().attribute( "todoItem", hasProperty("id", equalTo(TODO_ITEM_ID)) )); } @Test @DisplayName("Should display the correct title and description") void shouldDisplayCorrectTitleAndDescription() throws Exception { requestBuilder.findById(TODO_ITEM_ID) .andExpect(model().attribute( "todoItem", allOf( hasProperty("title", equalTo(TITLE)), hasProperty("description",equalTo(DESCRIPTION)) ) )); } @Test @DisplayName("Should display an open todo item") void shouldDisplayOpenTodoItem() throws Exception { requestBuilder.findById(TODO_ITEM_ID) .andExpect(model().attribute( "todoItem", hasProperty("status", equalTo(STATUS_OPEN)) )); } @Test @DisplayName("Should display a todo item that has one tag") void shouldDisplayTodoItemThatHasOneTag() throws Exception { requestBuilder.findById(TODO_ITEM_ID) .andExpect(model().attribute( "todoItem", hasProperty("tags", hasSize(1)) )); } @Test @DisplayName("Should display the information of the found tag") void shouldDisplayInformationOfFoundTag() throws Exception { requestBuilder.findById(TODO_ITEM_ID) .andExpect(model().attribute( "todoItem", hasProperty("tags", hasItem( allOf( hasProperty("id", equalTo(TAG_ID)), hasProperty("name", equalTo(TAG_NAME)) ) )) )); } } } }
We can now write unit tests for a controller method that renders the information of a single item. Let's summarize what we learned from this blog post.
Summary
This blog post has taught us four things:
- When we want to write assertions for the returned HTTP status, we have to invoke the
status()
method of theMockMvcResultMatchers
class. - When we want to write assertions for the rendered view, we have to invoke the
view()
method of theMockMvcResultMatchers
class. - When we want to write assertions for the Spring MVC model, we have to invoke the
model()
method of theMockMvcResultMatchers
class. - We can use Hamcrest matchers for writing assertions for the model attributes found from the Spring MVC model.
P.S. You can get the example application of this blog post from Github.