This blog post is outdated! If you want to learn how you can write unit tests for Spring MVC controllers, you should take a look at my updated Spring MVC Test tutorial. It describes how you can write unit tests for Spring MVC controllers with JUnit 5.

The first part of this tutorial described how we can configure our unit tests which use the Spring MVC Test framework.

Now it is time to get our hands dirty and learn how we can write unit tests for “normal” controllers.

The obvious next question is

What is a normal controller?

Well, a normal controller (in the context of this blog post) is a controller which either renders a view or handles form submissions.

Let’s get started.

I recommend that you read the first part of this tutorial before reading this blog post (If you have already read it, you are allowed to continue reading)

Getting The Required Dependencies with Maven

We can get the required testing dependencies by adding the following dependency declarations to the POM file of our example application:

  • Jackson 2.2.1 (core and databind modules). We use Jackson to transform objects into url encoded String objects.
  • Hamcrest 1.3. We use Hamcrest matchers when we are writing assertions for the responses.
  • JUnit 4.11 (exclude the hamcrest-core dependency).
  • Mockito 1.9.5
  • Spring Test 3.2.3.RELEASE

The relevant part of our pom.xml file looks as follows:

 1<dependency>
 2    <groupId>com.fasterxml.jackson.core</groupId>
 3    <artifactId>jackson-core</artifactId>
 4    <version>2.2.1</version>
 5    <scope>test</scope>
 6</dependency>
 7<dependency>
 8    <groupId>com.fasterxml.jackson.core</groupId>
 9    <artifactId>jackson-databind</artifactId>
10    <version>2.2.1</version>
11    <scope>test</scope>
12</dependency>
13<dependency>
14    <groupId>org.hamcrest</groupId>
15    <artifactId>hamcrest-all</artifactId>
16    <version>1.3</version>
17    <scope>test</scope>
18</dependency>
19<dependency>
20    <groupId>junit</groupId>
21    <artifactId>junit</artifactId>
22    <version>4.11</version>
23    <scope>test</scope>
24    <exclusions>
25        <exclusion>
26            <artifactId>hamcrest-core</artifactId>
27            <groupId>org.hamcrest</groupId>
28        </exclusion>
29    </exclusions>
30</dependency>
31<dependency>
32    <groupId>org.mockito</groupId>
33    <artifactId>mockito-core</artifactId>
34    <version>1.9.5</version>
35    <scope>test</scope>
36</dependency>
37<dependency>
38	<groupId>org.springframework</groupId>
39	<artifactId>spring-test</artifactId>
40	<version>3.2.3.RELEASE</version>
41	<scope>test</scope>
42</dependency>

Let’s move on and find out how we can write unit tests for Spring MVC controllers by using the Spring MVC Test framework.

Writing Unit Tests for Controller Methods

Every unit test which we write to test the behavior of a controller method consists of these steps:

  1. We send a request to the tested controller method.
  2. We verify that we received the expected response.

The Spring MVC Test framework has a few “core” classes which we can use for implementing these steps in our tests. These classes are described in the following:

  • We can build our requests by using the static methods of the MockMvcRequestBuilders class. Or to be more specific, we can create request builders which are then passed as a method parameter to the method which executes the actual request.
  • The MockMvc class is the main entry point of our tests. We can execute requests by calling its perform(RequestBuilder requestBuilder) method.
  • We can write assertions for the received response by using the static methods of the MockMvcResultMatchers class.

Next we will take a look at some examples which demonstrates how we can use these classes in our unit tests. We will write unit tests for the following controller methods:

  • The first controller method renders a page which shows a list of todo entries.
  • The second controller method renders a page which shows the information of a single todo entry.
  • The third controller method handles form submissions of the form which is used to add new todo entries to the database.

Rendering The Todo Entry List Page

Let’s start by taking a look at the implementation of the controller method which is used to render the todo entry list page.

Excpected Behavior

The implementation of the controller method which is used to show the information of all todo entries has the following steps:

  1. It processes GET requests send to url ‘/’.
  2. It gets the todo entries by calling the findAll() method of the TodoService interface. This method returns a list of Todo objects.
  3. It adds the received list to the model.
  4. It returns the name of the rendered view.

The relevant part of the TodoController class looks as follows:

 1import org.springframework.stereotype.Controller;
 2import org.springframework.ui.Model;
 3import org.springframework.web.bind.annotation.*;
 4import java.util.List;
 5
 6@Controller
 7public class TodoController {
 8
 9    private final TodoService service;
10
11    @RequestMapping(value = "/", method = RequestMethod.GET)
12    public String findAll(Model model) {
13        List<Todo> models = service.findAll();
14        model.addAttribute("todos", models);
15        return "todo/list";
16    }
17}

We are now ready to write an unit test for this method. Let’s see how we can do it.

Test: Todo Entries Are Found

We can write an unit test for this controller method by following steps:

  1. Create the test data which is returned when our service method is called. We use a concept called test data builder when we are creating the test data for our test.
  2. Configure the used mock object to return the created test data when its findAll() method is called.
  3. Execute a GET request to url ‘/’.
  4. Ensure that the HTTP status code 200 is returned.
  5. Ensure that the name of the returned view is ’todo/list'.
  6. Ensure that the request is forwarded to url ‘/WEB-INF/jsp/todo/list.jsp’.
  7. Ensure that model attribute called todos has two items in it.
  8. Ensure that the model attribute called todos contains the correct items.
  9. Verify that the findAll() method of our mock object was called only once.
  10. Ensure that other methods of the mock object were not called during the test.

The source code of our unit test looks as follows:

 1import org.junit.Before;
 2import org.junit.Test;
 3import org.junit.runner.RunWith;
 4import org.mockito.Mockito;
 5import org.springframework.beans.factory.annotation.Autowired;
 6import org.springframework.test.context.ContextConfiguration;
 7import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 8import org.springframework.test.context.web.WebAppConfiguration;
 9import org.springframework.test.web.servlet.MockMvc;
10import org.springframework.test.web.servlet.setup.MockMvcBuilders;
11import org.springframework.web.context.WebApplicationContext;
12
13import java.util.Arrays;
14
15import static org.hamcrest.Matchers.*;
16import static org.hamcrest.Matchers.is;
17import static org.mockito.Mockito.*;
18import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
19import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
20import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model;
21
22@RunWith(SpringJUnit4ClassRunner.class)
23@ContextConfiguration(classes = {TestContext.class, WebAppContext.class})
24@WebAppConfiguration
25public class TodoControllerTest {
26
27    private MockMvc mockMvc;
28
29    @Autowired
30    private TodoService todoServiceMock;
31
32    //Add WebApplicationContext field here
33
34    //The setUp() method is omitted.
35
36    @Test
37    public void findAll_ShouldAddTodoEntriesToModelAndRenderTodoListView() throws Exception {
38        Todo first = new TodoBuilder()
39                .id(1L)
40                .description("Lorem ipsum")
41                .title("Foo")
42                .build();
43
44        Todo second = new TodoBuilder()
45                .id(2L)
46                .description("Lorem ipsum")
47                .title("Bar")
48                .build();
49
50        when(todoServiceMock.findAll()).thenReturn(Arrays.asList(first, second));
51
52        mockMvc.perform(get("/"))
53                .andExpect(status().isOk())
54                .andExpect(view().name("todo/list"))
55                .andExpect(forwardedUrl("/WEB-INF/jsp/todo/list.jsp"))
56                .andExpect(model().attribute("todos", hasSize(2)))
57                .andExpect(model().attribute("todos", hasItem(
58                        allOf(
59                                hasProperty("id", is(1L)),
60                                hasProperty("description", is("Lorem ipsum")),
61                                hasProperty("title", is("Foo"))
62                        )
63                )))
64                .andExpect(model().attribute("todos", hasItem(
65                        allOf(
66                                hasProperty("id", is(2L)),
67                                hasProperty("description", is("Lorem ipsum")),
68                                hasProperty("title", is("Bar"))
69                        )
70                )));
71
72        verify(todoServiceMock, times(1)).findAll();
73        verifyNoMoreInteractions(todoServiceMock);
74    }
75}

Rendering The View Todo Entry Page

Before we can write the actual unit tests for our controller method, we have to take a closer look at the implementation of that method.

Let’s move on and find out how our controller is implemented.

Expected Behavior

The controller method which is used to show the information of a single todo entry is implemented by following these steps:

  1. It processes GET requests send to url ‘/todo/{id}’. The {id} is a path variable which contains the id of the requested todo entry.
  2. It obtains the requested todo entry by calling the findById() method of the TodoService interface and passes the id of the requested todo entry as a method parameter. This method returns the found todo entry. If no todo entry is found, this method throws a TodoNotFoundException.
  3. It adds the found todo entry to the model.
  4. It returns the name of the rendered view.

The source code of our controller method looks as follows:

 1import org.springframework.stereotype.Controller;
 2import org.springframework.ui.Model;
 3import org.springframework.web.bind.annotation.*;
 4
 5@Controller
 6public class TodoController {
 7
 8    private final TodoService service;
 9
10    @RequestMapping(value = "/todo/{id}", method = RequestMethod.GET)
11    public String findById(@PathVariable("id") Long id, Model model) throws TodoNotFoundException {
12        Todo found = service.findById(id);
13        model.addAttribute("todo", found);
14        return "todo/view";
15    }
16}

Our next question is:

What happens when a TodoNotFoundException is thrown?

In the previous part of this tutorial, we created an exception resolver bean which is used to handle exceptions thrown by our controller classes. The configuration of this bean looks as follows:

 1@Bean
 2public SimpleMappingExceptionResolver exceptionResolver() {
 3	SimpleMappingExceptionResolver exceptionResolver = new SimpleMappingExceptionResolver();
 4
 5	Properties exceptionMappings = new Properties();
 6
 7	exceptionMappings.put("net.petrikainulainen.spring.testmvc.todo.exception.TodoNotFoundException", "error/404");
 8	exceptionMappings.put("java.lang.Exception", "error/error");
 9	exceptionMappings.put("java.lang.RuntimeException", "error/error");
10
11	exceptionResolver.setExceptionMappings(exceptionMappings);
12
13	Properties statusCodes = new Properties();
14
15	statusCodes.put("error/404", "404");
16	statusCodes.put("error/error", "500");
17
18	exceptionResolver.setStatusCodes(statusCodes);
19
20	return exceptionResolver;
21}

As we can see, if a TodoNotFoundException is thrown, our application renders the ’error/404’ view and returns the HTTP status code 404.

It is clear that we have to write two tests for this controller method:

  1. We have to write a test which ensures that our application is working correctly when the todo entry is not found.
  2. We have to write a test which verifies that our application is working correctly when the todo entry is found.

Let’s see how we can write these tests.

Test 1: Todo Entry Is Not Found

First, we must ensure that our application is working property when the requested todo entry is not found. We can write the test which ensures this by following these steps:

  1. Configure the mock object to throw a TodoNotFoundException when its findById() method is called and the id of the requested todo entry is 1L.
  2. Execute a GET request to url ‘/todo/1’.
  3. Verify that the HTTP status code 404 is returned.
  4. Ensure that the name of the returned view is ’error/404’.
  5. Ensure that the request is forwarded to url ‘/WEB-INF/jsp/error/404.jsp’.
  6. Verify that the findById() method of the TodoService interface is called only once with the correct method parameter (1L).
  7. Verify that no other methods of the mock object were called during this test.

The source code of our unit test looks as follows:

 1import org.junit.Before;
 2import org.junit.Test;
 3import org.junit.runner.RunWith;
 4import org.mockito.Mockito;
 5import org.springframework.beans.factory.annotation.Autowired;
 6import org.springframework.test.context.ContextConfiguration;
 7import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 8import org.springframework.test.context.web.WebAppConfiguration;
 9import org.springframework.test.web.servlet.MockMvc;
10import org.springframework.test.web.servlet.setup.MockMvcBuilders;
11import org.springframework.web.context.WebApplicationContext;
12
13import static org.mockito.Mockito.*;
14import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
15import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
16
17@RunWith(SpringJUnit4ClassRunner.class)
18@ContextConfiguration(classes = {TestContext.class, WebAppContext.class})
19@WebAppConfiguration
20public class TodoControllerTest {
21
22    private MockMvc mockMvc;
23
24    @Autowired
25    private TodoService todoServiceMock;
26
27    //Add WebApplicationContext field here
28
29    //The setUp() method is omitted.
30
31    @Test
32    public void findById_TodoEntryNotFound_ShouldRender404View() throws Exception {
33        when(todoServiceMock.findById(1L)).thenThrow(new TodoNotFoundException(""));
34
35        mockMvc.perform(get("/todo/{id}", 1L))
36                .andExpect(status().isNotFound())
37                .andExpect(view().name("error/404"))
38                .andExpect(forwardedUrl("/WEB-INF/jsp/error/404.jsp"));
39
40        verify(todoServiceMock, times(1)).findById(1L);
41        verifyZeroInteractions(todoServiceMock);
42    }
43}

Test 2: Todo Entry Is Found

Second, we must write a test which ensures that our controller is working properly when a todo entry is found. We can do this by following these steps:

  1. Create the Todo object which is returned when our service method is called. Again, we create the returned Todo object by using our test data builder.
  2. Configure our mock object to return the created Todo object when its findById() method is called by using a method parameter 1L.
  3. Execute a GET request to url ‘/todo/1’.
  4. Verify that the HTTP status code 200 is returned.
  5. Ensure that the name of the returned view is ’todo/view'.
  6. Ensure that the request is forwarded to url ‘/WEB-INF/jsp/todo/view.jsp’.
  7. Verify that the id of the model object called todo is 1L.
  8. Verify that the description of the model object called todo is ‘Lorem ipsum’.
  9. Verify that the title of the model object called todo is ‘Foo’.
  10. Ensure that the findById() method of our mock object is called only once with the correct method parameter (1L).
  11. Ensure that the other methods of the mock object were not called during our test.

The source code of our unit test looks as follows:

 1import org.junit.Before;
 2import org.junit.Test;
 3import org.junit.runner.RunWith;
 4import org.mockito.Mockito;
 5import org.springframework.beans.factory.annotation.Autowired;
 6import org.springframework.test.context.ContextConfiguration;
 7import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 8import org.springframework.test.context.web.WebAppConfiguration;
 9import org.springframework.test.web.servlet.MockMvc;
10import org.springframework.test.web.servlet.setup.MockMvcBuilders;
11import org.springframework.web.context.WebApplicationContext;
12
13import static org.hamcrest.Matchers.hasProperty;
14import static org.hamcrest.Matchers.is;
15import static org.mockito.Mockito.*;
16import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
17import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
18
19@RunWith(SpringJUnit4ClassRunner.class)
20@ContextConfiguration(classes = {TestContext.class, WebAppContext.class})
21@WebAppConfiguration
22public class TodoControllerTest {
23
24    private MockMvc mockMvc;
25
26    @Autowired
27    private TodoService todoServiceMock;
28
29    //Add WebApplicationContext field here
30
31    //The setUp() method is omitted.
32
33    @Test
34    public void findById_TodoEntryFound_ShouldAddTodoEntryToModelAndRenderViewTodoEntryView() throws Exception {
35        Todo found = new TodoBuilder()
36                .id(1L)
37                .description("Lorem ipsum")
38                .title("Foo")
39                .build();
40
41        when(todoServiceMock.findById(1L)).thenReturn(found);
42
43        mockMvc.perform(get("/todo/{id}", 1L))
44                .andExpect(status().isOk())
45                .andExpect(view().name("todo/view"))
46                .andExpect(forwardedUrl("/WEB-INF/jsp/todo/view.jsp"))
47                .andExpect(model().attribute("todo", hasProperty("id", is(1L))))
48                .andExpect(model().attribute("todo", hasProperty("description", is("Lorem ipsum"))))
49                .andExpect(model().attribute("todo", hasProperty("title", is("Foo"))));
50
51        verify(todoServiceMock, times(1)).findById(1L);
52        verifyNoMoreInteractions(todoServiceMock);
53    }
54}

Handling The Form Submission of The Add Todo Entry Form

Again, we will first take a look at the expected behavior of our controller method before we will write the unit tests for it.

Expected Behavior

The controller method which handles the form submissions of the add todo entry form is implemented by following these steps:

  1. It processes POST requests send to url ‘/todo/add’.
  2. It checks that the BindingResult object given as a method parameter doesn’t have any errors. If errors are found, it returns the name of the form view.
  3. It adds a new Todo entry by calling the add() method of the TodoService interface and passes the form object as a method parameter. This method creates a new todo entry and returns it.
  4. It creates the feedback message about the added todo entry and adds the message to the RedirectAttributes object given as a method parameter.
  5. It adds the id of the added todo entry to the RedirectAttributes object.
  6. It returns the name of a redirect view which redirects the request to the view todo entry page.

The relevant part of the TodoController class looks as follows:

 1import org.springframework.context.MessageSource;
 2import org.springframework.context.i18n.LocaleContextHolder;
 3import org.springframework.stereotype.Controller;
 4import org.springframework.validation.BindingResult;
 5import org.springframework.web.bind.annotation.*;
 6import org.springframework.web.servlet.mvc.support.RedirectAttributes;
 7
 8import javax.validation.Valid;
 9import java.util.Locale;
10
11@Controller
12@SessionAttributes("todo")
13public class TodoController {
14
15    private final TodoService service;
16
17    private final MessageSource messageSource;
18
19    @RequestMapping(value = "/todo/add", method = RequestMethod.POST)
20    public String add(@Valid @ModelAttribute("todo") TodoDTO dto, BindingResult result, RedirectAttributes attributes) {
21        if (result.hasErrors()) {
22            return "todo/add";
23        }
24
25        Todo added = service.add(dto);
26
27        addFeedbackMessage(attributes, "feedback.message.todo.added", added.getTitle());
28        attributes.addAttribute("id", added.getId());
29
30        return createRedirectViewPath("/todo/{id}");
31    }
32
33    private void addFeedbackMessage(RedirectAttributes attributes, String messageCode, Object... messageParameters) {
34        String localizedFeedbackMessage = getMessage(messageCode, messageParameters);
35        attributes.addFlashAttribute("feedbackMessage", localizedFeedbackMessage);
36    }
37
38    private String getMessage(String messageCode, Object... messageParameters) {
39        Locale current = LocaleContextHolder.getLocale();
40        return messageSource.getMessage(messageCode, messageParameters, current);
41    }
42
43    private String createRedirectViewPath(String requestMapping) {
44        StringBuilder redirectViewPath = new StringBuilder();
45        redirectViewPath.append("redirect:");
46        redirectViewPath.append(requestMapping);
47        return redirectViewPath.toString();
48    }
49}

As we can see, the controller method uses a TodoDTO object as a form object. The TodoDTO class is a simple DTO class which source code looks as follows:

 1import org.hibernate.validator.constraints.Length;
 2import org.hibernate.validator.constraints.NotEmpty;
 3
 4public class TodoDTO {
 5
 6    private Long id;
 7
 8    @Length(max = 500)
 9    private String description;
10
11    @NotEmpty
12    @Length(max = 100)
13    private String title;
14
15	//Constructor and other methods are omitted.
16}

The TodoDTO class declares some validation constraints which are described in following:

  • The title of a todo entry cannot be empty.
  • The maximum length of the description is 500 characters.
  • The maximum length of the title is 100 characters.

If we think about the tests which we should write for this controller method, it is clear that we must ensure that

  1. The controller method is working property when the validation fails.
  2. The controller method is working property when a todo entry is added to the database.

Let’s find out how we can write these tests.

Test 1: Validation Fails

First, we have to write a test which ensures that our controller method is working properly when the validation fails. We can write this test by following these steps:

  1. Create a title which has 101 characters.
  2. Create a description which has 501 characters.
  3. Execute a POST request to url ‘/todo/add’ by following these steps:
  4. Set the content type of the request to ‘application/x-www-form-urlencoded’.
  5. Send the description and title of the todo entry as request parameters.
  6. Set a new TodoDTO object to session. This is required because our controller is annotated with the @SessionAttributes annotation.
  7. Verify that the HTTP status code 200 is returned.
  8. Verify that the name of the returned view is ’todo/add’.
  9. Verify that the request is forwarded to url ‘/WEB-INF/jsp/todo/add.jsp’.
  10. Verify that our model attribute has field errors in the title and description fields.
  11. Ensure that the id of our model attribute is null.
  12. Ensure that the description of our model attribute is correct.
  13. Ensure that the title of our model attribute is correct.
  14. Ensure that the methods of our mock object were not called during the test.

The source code of our unit test looks as follows:

 1import org.junit.Before;
 2import org.junit.Test;
 3import org.junit.runner.RunWith;
 4import org.mockito.Mockito;
 5import org.springframework.beans.factory.annotation.Autowired;
 6import org.springframework.http.MediaType;
 7import org.springframework.test.context.ContextConfiguration;
 8import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 9import org.springframework.test.context.web.WebAppConfiguration;
10import org.springframework.test.web.servlet.MockMvc;
11import org.springframework.test.web.servlet.setup.MockMvcBuilders;
12import org.springframework.web.context.WebApplicationContext;
13
14import static org.hamcrest.Matchers.hasProperty;
15import static org.hamcrest.Matchers.is;
16import static org.hamcrest.Matchers.nullValue;
17import static org.mockito.Mockito.*;
18import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
19import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
20
21@RunWith(SpringJUnit4ClassRunner.class)
22@ContextConfiguration(classes = {TestContext.class, WebAppContext.class})
23@WebAppConfiguration
24public class TodoControllerTest {
25
26    private MockMvc mockMvc;
27
28    @Autowired
29    private TodoService todoServiceMock;
30
31    //Add WebApplicationContext field here
32
33    //The setUp() method is omitted.
34
35    @Test
36    public void add_DescriptionAndTitleAreTooLong_ShouldRenderFormViewAndReturnValidationErrorsForTitleAndDescription() throws Exception {
37        String title = TestUtil.createStringWithLength(101);
38        String description = TestUtil.createStringWithLength(501);
39
40        mockMvc.perform(post("/todo/add")
41                .contentType(MediaType.APPLICATION_FORM_URLENCODED)
42                .param("description", description)
43                .param("title", title)
44                .sessionAttr("todo", new TodoDTO())
45        )
46                .andExpect(status().isOk())
47                .andExpect(view().name("todo/add"))
48                .andExpect(forwardedUrl("/WEB-INF/jsp/todo/add.jsp"))
49                .andExpect(model().attributeHasFieldErrors("todo", "title"))
50                .andExpect(model().attributeHasFieldErrors("todo", "description"))
51                .andExpect(model().attribute("todo", hasProperty("id", nullValue())))
52                .andExpect(model().attribute("todo", hasProperty("description", is(description))))
53                .andExpect(model().attribute("todo", hasProperty("title", is(title))));
54
55        verifyZeroInteractions(todoServiceMock);
56    }
57}

Our test case calls the static createStringWithLength(int length) method of the TestUtil class. This method creates a new String object with the given length and returns the created object.

The source code of the TestUtil class looks as follows:

 1import com.fasterxml.jackson.annotation.JsonInclude;
 2import com.fasterxml.jackson.databind.ObjectMapper;
 3
 4import java.util.Iterator;
 5import java.util.Map;
 6import java.util.Set;
 7
 8public class TestUtil {
 9
10    public static String createStringWithLength(int length) {
11        StringBuilder builder = new StringBuilder();
12
13        for (int index = 0; index < length; index++) {
14            builder.append("a");
15        }
16
17        return builder.toString();
18    }
19}

Test 2: Todo Entry Is Added to The Database

Second, we have to write a test which ensures that our controller is working properly when a new todo entry is added to the database. We can write this test by following these steps:

  1. Create a Todo object which is returned when the add() method of the TodoService interface is called.
  2. Configure our mock object to return the created Todo object when its add() method is called a TodoDTO object is given as a method parameter.
  3. Execute a POST request to url ‘/todo/add’ by following these steps:
  4. Set the content type of the request to ‘application/x-www-form-urlencoded’.
  5. Send the description and title of the todo entry as request parameters.
  6. Set a new TodoDTO object to session. This is required because our controller is annotated with the @SessionAttributes annotation.
  7. Verify that the HTTP status code 302 is returned.
  8. Ensure that the name of the returned view is ‘redirect:todo/{id}’.
  9. Ensure that the request is redirected to url ‘/todo/1’.
  10. Verify that the model attribute called id is ‘1’.
  11. Verify that the feedback message is set.
  12. Verify that the add() method of our mock object is called only once and that a TodoDTO object was given as a method parameter. Capture the object given as a method parameter by using an ArgumentCaptor object.
  13. Verify that no other methods of the mock object were called during our test.
  14. Verify that the field values of the TodoDTO object are correct.

The source code of our unit test looks as follows:

 1import org.junit.Before;
 2import org.junit.Test;
 3import org.junit.runner.RunWith;
 4import org.mockito.ArgumentCaptor;
 5import org.mockito.Mockito;
 6import org.springframework.beans.factory.annotation.Autowired;
 7import org.springframework.http.MediaType;
 8import org.springframework.test.context.ContextConfiguration;
 9import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
10import org.springframework.test.context.web.WebAppConfiguration;
11import org.springframework.test.web.servlet.MockMvc;
12import org.springframework.test.web.servlet.setup.MockMvcBuilders;
13import org.springframework.web.context.WebApplicationContext;
14
15import static org.hamcrest.Matchers.is;
16import static org.junit.Assert.assertNull;
17import static org.junit.Assert.assertThat;
18import static org.mockito.Matchers.isA;
19import static org.mockito.Mockito.*;
20import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
21import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
22
23@RunWith(SpringJUnit4ClassRunner.class)
24@ContextConfiguration(classes = {TestContext.class, WebAppContext.class})
25@WebAppConfiguration
26public class TodoControllerTest {
27
28    private MockMvc mockMvc;
29
30    @Autowired
31    private TodoService todoServiceMock;
32
33    //Add WebApplicationContext field here
34
35    //The setUp() method is omitted.
36
37    @Test
38    public void add_NewTodoEntry_ShouldAddTodoEntryAndRenderViewTodoEntryView() throws Exception {
39        Todo added = new TodoBuilder()
40                .id(1L)
41                .description("description")
42                .title("title")
43                .build();
44
45        when(todoServiceMock.add(isA(TodoDTO.class))).thenReturn(added);
46
47        mockMvc.perform(post("/todo/add")
48                .contentType(MediaType.APPLICATION_FORM_URLENCODED)
49                .param("description", "description")
50                .param("title", "title")
51                .sessionAttr("todo", new TodoDTO())
52        )
53                .andExpect(status().isMovedTemporarily())
54                .andExpect(view().name("redirect:todo/{id}"))
55                .andExpect(redirectedUrl("/todo/1"))
56                .andExpect(model().attribute("id", is("1")))
57                .andExpect(flash().attribute("feedbackMessage", is("Todo entry: title was added.")));
58
59		ArgumentCaptor<TodoDTO> formObjectArgument = ArgumentCaptor.forClass(TodoDTO.class);
60		verify(todoServiceMock, times(1)).add(formObjectArgument.capture());
61		verifyNoMoreInteractions(todoServiceMock);
62
63		TodoDTO formObject = formObjectArgument.getValue();
64
65		assertThat(formObject.getDescription(), is("description"));
66		assertNull(formObject.getId());
67		assertThat(formObject.getTitle(), is("title"));
68    }
69}

Summary

We have now written some unit tests for “normal” controller methods by using the Spring MVC Test framework. This tutorial has taught has four things:

  • We learned to create requests which are processed by the tested controller methods.
  • We learned to write assertions for the responses returned by the tested controller methods.
  • We learned how we can write unit tests for controller methods which render a view.
  • We learned to write unit tests for controller methods which handle form submissions.

The next part of this tutorial describes how we can write unit tests for a REST API.

P.S. You can get the example application of this blog post from Github. I recommend that you check it out because it has some unit tests which were not covered in this blog post.