Writing Unit Tests for Spring MVC Controllers: Rendering a List

The previous part of my Spring MVC Test tutorial described how we can write unit tests for Spring MVC controllers which render the information of a single item. This blog post provides more information about writing unit tests for Spring MVC controllers which render a view. To be more specific, this blog post describes how we can write unit tests for a Spring MVC controller that renders a list.

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'. This method returns the HTTP status code 200 and renders the information of all todo items which are found from the database. If no todo items are found from the database, this controller method returns the HTTP status code 200 and renders an empty list.

The tested controller method is called findAll() and it's implemented by following these steps:

  1. Find the todo items from the database by invoking the findAll() method of the TodoItemCrudService class.
  2. Put the found todo items to a model attribute called todoItems.
  3. Return the name of the view ('todo-item/list') that renders the information of the found todo items.

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.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
    public String findAll(Model model) {
        List<TodoListItemDTO> todoItems = service.findAll();
        model.addAttribute("todoItems", todoItems);
        return "todo-item/list";
    }
}

The TodoListItemDTO class is a DTO that contains the information of a single todo item. Its source code looks as follows:

public class TodoListItemDTO {

    private Long id;
    private String title;
    private TodoItemStatus status;

    //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 a StatusResultMatchers object which allows us to write assertions for the returned HTTP status.
  • The view() method returns a ViewResultMatchers object which allows us to write assertions for the rendered view.
  • The model() method returns a ModelResultMatchers 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:

  1. Add a new method called findAll() to our request builder class and ensure that this method returns a ResultActions object.
  2. Send a GET request to the path: '/todo-item' by invoking the perform() method of the MockMvc class. Remember to return the ResultActions object that's returned by the perform() 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 findAll() throws Exception {
        return mockMvc.perform(get("/todo-item"));
    }
}

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:

  1. Add an inner class called FindAll to our test class. This inner class contains the test methods which ensure that the system under test is working as expected.
  2. Add an inner class called WhenNoTodoItemsAreFoundFromDatabase to the FindAll class. This inner class contains the test methods which ensure that the system under test is working as expected when no todo items are found from the database.
  3. Add an inner class called WhenTwoTodoItemsAreFoundFromDatabase to the FindAll class. This inner class contains the test methods which ensure that the system under test is working as expected when two todo items are 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 information of all todo items")
    class FindAll {
        
        @Nested
        @DisplayName("When no todo items are found from the database")
        class WhenNoTodoItemsAreFoundFromDatabase {
            
        }

        @Nested
        @DisplayName("When two todo items are found from the database")
        class WhenTwoTodoItemsAreFoundFromDatabase {
            
        }
    }
}

Second, because we don't want to add duplicate code to our test class, we will add some test methods to the FindAll class. These unit tests specify the behavior of the system under test in all possible scenarios. We can write these unit tests by following these steps:

  1. Ensure that the system under test returns the HTTP status code 200.
  2. Verify that the system under test renders the list view.

After we have written these unit tests, 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.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 all todo items")
    class FindAll {

        @Test
        @DisplayName("Should return the HTTP status code 200")
        void shouldReturnHttpStatusCodeOk() throws Exception {
            requestBuilder.findAll().andExpect(status().isOk());
        }

        @Test
        @DisplayName("Should render the todo item list view")
        void shouldRenderTodoItemListView() throws Exception {
            requestBuilder.findAll().andExpect(view().name("todo-item/list"));
        }

        //The inner classes are omitted
    }
}

Third, we have to write the unit tests which ensure that the system under test is working as expected when no todo items are found from the database. We can write the required test methods by following these steps:

  1. Add a new setup method to the WhenNoTodoItemsAreFoundFromDatabase class and ensure that it's run before a test method is run. When we implement this method, we must ensure that the TodoItemCrudService object returns an empty list when its findAll() method is invoked.
  2. Ensure that the system under test displays zero todo items.

After we have written the required unit tests, 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.ArrayList;

import static net.petrikainulainen.springmvctest.junit5.web.WebTestConfig.*;
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 all todo items")
    class FindAll {

        @Test
        @DisplayName("Should return the HTTP status code 200")
        void shouldReturnHttpStatusCodeOk() throws Exception {
            requestBuilder.findAll().andExpect(status().isOk());
        }

        @Test
        @DisplayName("Should render the todo item list view")
        void shouldRenderTodoItemListView() throws Exception {
            requestBuilder.findAll().andExpect(view().name("todo-item/list"));
        }

        @Nested
        @DisplayName("When no todo items are found from the database")
        class WhenNoTodoItemsAreFoundFromDatabase {

            @BeforeEach
            void serviceReturnsEmptyList() {
                given(service.findAll()).willReturn(new ArrayList<>());
            }

            @Test
            @DisplayName("Should display zero todo items")
            void shouldDisplayZeroTodoItems() throws Exception {
                requestBuilder.findAll().andExpect(model().attribute(
                        "todoItems", 
                        hasSize(0)
                ));
            }
        }

        //The other inner class is omitted
    }
}

Fourth, we have to write the unit tests which ensure that the system under test is working as expected when two todo items are found from the database. We can write the required test methods by following these steps:

  1. Add the required constants to the WhenTwoTodoItemsAreFoundFromDatabase class. These constants specify the information of the found todo items.
  2. Add a new setup method to the WhenTwoTodoItemsAreFoundFromDatabase class and ensure that it's run before a test method is run. When we implement this method, we must ensure that the TodoItemCrudService object returns a list that contains two todo items when its findAll() method is invoked.
  3. Ensure that the system under test displays two todo items.
  4. Verify that the system under test displays the correct information of the first todo item.
  5. Ensure that the system under test displays the correct information of the second todo item.
  6. Verify that the system under test displays the todo items in the correct order.

After we have written the required unit tests, 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.Arrays;

import static net.petrikainulainen.springmvctest.junit5.web.WebTestConfig.*;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.contains;
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 all todo items")
    class FindAll {

        @Test
        @DisplayName("Should return the HTTP status code 200")
        void shouldReturnHttpStatusCodeOk() throws Exception {
            requestBuilder.findAll().andExpect(status().isOk());
        }

        @Test
        @DisplayName("Should render the todo item list view")
        void shouldRenderTodoItemListView() throws Exception {
            requestBuilder.findAll().andExpect(view().name("todo-item/list"));
        }

        //The other inner class is omitted

        @Nested
        @DisplayName("When two todo items are found from the database")
        class WhenTwoTodoItemsAreFoundFromDatabase {

            private final Long TODO_ITEM_ONE_ID = 1L;
            private final String TODO_ITEM_ONE_TITLE = "first todo item";
            private final Long TODO_ITEM_TWO_ID = 2L;
            private final String TODO_ITEM_TWO_TITLE = "second todo item";

            private final TodoItemStatus STATUS_OPEN = TodoItemStatus.OPEN;

            @BeforeEach
            void serviceReturnsTwoTodoItems() {
                TodoListItemDTO first = new TodoListItemDTO();
                first.setId(TODO_ITEM_ONE_ID);
                first.setTitle(TODO_ITEM_ONE_TITLE);
                first.setStatus(STATUS_OPEN);

                TodoListItemDTO second = new TodoListItemDTO();
                second.setId(TODO_ITEM_TWO_ID);
                second.setTitle(TODO_ITEM_TWO_TITLE);
                second.setStatus(STATUS_OPEN);

                given(service.findAll()).willReturn(Arrays.asList(first, second));
            }

            @Test
            @DisplayName("Should display two todo items")
            void shouldDisplayTwoTodoItems() throws Exception {
                requestBuilder.findAll().andExpect(model().attribute(
                        "todoItems",
                        hasSize(2)
                ));
            }

            @Test
            @DisplayName("Should display the information of the first todo item")
            void shouldDisplayInformationOfFirstTodoItem() throws Exception {
                requestBuilder.findAll()
                        .andExpect(
                                model().attribute(
                                        "todoItems", 
                                        hasItem(allOf(
                                                hasProperty("id", equalTo(TODO_ITEM_ONE_ID)),
                                                hasProperty("title", equalTo(TODO_ITEM_ONE_TITLE)),
                                                hasProperty("status", equalTo(STATUS_OPEN))
                                        ))
                                )
                        );
            }

            @Test
            @DisplayName("Should display the information of the second todo item")
            void shouldDisplayInformationOfSecondTodoItem() throws Exception {
                requestBuilder.findAll()
                        .andExpect(
                                model().attribute(
                                        "todoItems",
                                        hasItem(allOf(
                                                hasProperty("id", equalTo(TODO_ITEM_TWO_ID)),
                                                hasProperty("title", equalTo(TODO_ITEM_TWO_TITLE)),
                                                hasProperty("status", equalTo(STATUS_OPEN))
                                        ))
                                )
                        );
            }

            @Test
            @DisplayName("Should display the todo items in the correct order")
            void shouldDisplayFirstAndSecondTodoItemInCorrectOrder() throws Exception {
                requestBuilder.findAll()
                        .andExpect(
                                model().attribute(
                                        "todoItems",
                                        contains(
                                                allOf(
                                                        hasProperty("id", equalTo(TODO_ITEM_ONE_ID)),
                                                        hasProperty("title", equalTo(TODO_ITEM_ONE_TITLE)),
                                                        hasProperty("status", equalTo(STATUS_OPEN))
                                                ),
                                                allOf(
                                                        hasProperty("id", equalTo(TODO_ITEM_TWO_ID)),
                                                        hasProperty("title", equalTo(TODO_ITEM_TWO_TITLE)),
                                                        hasProperty("status", equalTo(STATUS_OPEN))
                                                )
                                        )
                                )
                        );
            }
        }
    }
}

We can now write unit tests for a controller method that renders a list. 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 the MockMvcResultMatchers class.
  • When we want to write assertions for the rendered view, we have to invoke the view() method of the MockMvcResultMatchers class.
  • When we want to write assertions for the Spring MVC model, we have to invoke the model() method of the MockMvcResultMatchers 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.

0 comments… add one

Leave a Reply