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 aGET
?
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>
Hi,
What in case when controller has more dependencies except that one which is mocked? How to inject them into tested controller?
Regards,
Jakub
In that case, you can call
when(...).thenCallRealMethod();
on non-mocked dependencies. See this post for more info: https://myshittycode.com/2014/03/13/mockito-effective-partial-mocking/It works. Thanks.
In case personService.getPerson() returns List, how will you address this Mock test?
You can use
jsonPath(..)
from MockMvcResultMatchers to get all the persons. The second argument ofjsonPath(..)
takes a Hamcrest matcher. Now, you can use something likehasItems(..)
from Matchers to perform your assertions.