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>
Leave a Reply