Embracing the Messiness in Search of Epic Solutions

How to Unit Test Spring MVC Controller

Posted

in

SCENARIO

Let’s assume we have the following controller that needs to be tested:-

@Controller
@RequestMapping(value = "/person")
public class PersonController {

    private PersonService personService;

    @Autowired
    public PersonController(PersonService personService) {
        this.personService = personService;
    }

    @RequestMapping(value = "/{id}", method = RequestMethod.GET)
    public String getPerson(@PathVariable Long id, Model model) {
        model.addAttribute("personData", personService.getPerson(id));
        return "personPage";
    }
}

SOLUTION 1: “Works but It Won’t Get You the Promotion”

This working solution relies on:-

  • JUnit – General unit test framework.
  • Mockito – To mock PersonService.
public class PersonControllerTest {

    @Test
    public void testGetPerson() {

        PersonService personService = mock(PersonService.class);

        when(personService.getPerson(1L)).thenReturn(new Person(1L, "Chuck"));

        PersonController controller = new PersonController(personService);

        Model model = new ExtendedModelMap();

        String view = controller.getPerson(1L, model);

        assertEquals("View name", "personPage", view);

        Person actualPerson = (Person) model.asMap().get("personData");

        assertEquals("matching ID", Long.valueOf(1), actualPerson.getId());
        assertEquals("matching Name", "Chuck", actualPerson.getName());
    }
}

While this solution works, but it has a few problems. This test case strictly tests the actual controller API, but it completely disregards the URI and request method (GET, POST, PUT, DELETE, etc). For instance:-

  • What if the URI has a typo ( /persn/1 ) or it is not properly constructed ( /person/donkey-kong )?
  • What if the request method should be a POST but we accidentally used a GET?

SOLUTION 2: “A Mind Blowing Solution that Still Won’t Get You the Promotion but You Feel So Invincible That You Feel Compelled to Flip a Table Over”

This better solution relies on:-

  • JUnit – General unit test framework.
  • Mockito – To mock PersonService.
  • Spring MVC Test Framework – To properly test the controller.
  • Hamcrest – To clean way to assert the actual result is correct.
@RunWith(SpringJUnit4ClassRunner.class)
// This XML configuration basically enable component scanning.
// You could have used @Configuration and @ComponentScan to do the same thing.
@ContextConfiguration({"classpath*:spring-test.xml"})
public class PersonControllerTest {

    @Mock
    private PersonService personService;

    @InjectMocks
    private PersonController personController;

    private MockMvc mockMvc;

    @Before
    public void setup() {
        MockitoAnnotations.initMocks(this);
        mockMvc = MockMvcBuilders.standaloneSetup(personController).build();
    }

    @Test
    public void testGetPerson() throws Exception {
        when(personService.getPerson(1L)).thenReturn(new Person(1L, "Chuck"));

        mockMvc.perform(get("/person/{id}", 1L))
                .andExpect(status().isOk())
                .andExpect(view().name("personPage"))
                .andExpect(model().attribute("personData",
                                             allOf(hasProperty("id", is(1L)),
                                                   hasProperty("name", is("Chuck")))));
    }
}

Basically object that is annotated with @Mock will get injected into object that is annotated with InjectMocks. Then, we rely on Spring MVC Test Framework’s MockMvc to test our controller in a very clean and detailed fashion.

Okay, you may flip a table over now…

By the way…

You need at least Spring 3.2 to use MockMvc.

<dependency>
	<groupid>org.springframework</groupid>
	<artifactid>spring-test</artifactid>
	<version>3.2.4.RELEASE</version>
	<scope>test</scope>
</dependency>

If you are using an older Mockito version, you will get this error:-

org.mockito.exceptions.base.MockitoException: Field 'personController' annotated with @InjectMocks is null.
Please make sure the instance is created *before* MockitoAnnotations.initMocks();
Example of correct usage:
   class SomeTest {
      @InjectMocks private Foo foo = new Foo();

      @Before public void setUp() {
         MockitoAnnotations.initMock(this);

Upgrading Mockito to the latest version will fix this error:-

<dependency>
	<groupid>org.mockito</groupid>
	<artifactid>mockito-core</artifactid>
	<version>1.9.5</version>
	<scope>test</scope>
</dependency>

Comments

5 responses to “How to Unit Test Spring MVC Controller”

  1. Jakub Avatar
    Jakub

    Hi,

    What in case when controller has more dependencies except that one which is mocked? How to inject them into tested controller?

    Regards,
    Jakub

    1. Choon-Chern Lim Avatar

      In that case, you can call when(...).thenCallRealMethod(); on non-mocked dependencies. See this post for more info: http://myshittycode.com/2014/03/13/mockito-effective-partial-mocking/

  2. Jakub Avatar
    Jakub

    It works. Thanks.

  3. Sandeep Avatar
    Sandeep

    In case personService.getPerson() returns List, how will you address this Mock test?

    1. Choon-Chern Lim Avatar

      You can use jsonPath(..) from MockMvcResultMatchers to get all the persons. The second argument of jsonPath(..) takes a Hamcrest matcher. Now, you can use something like hasItems(..) from Matchers to perform your assertions.

Leave a Reply to Choon-Chern LimCancel reply