JUnit Testing in Spring MVC
1. Spring MVC projects include a test folder in src. You want to put all test cases in this folder. To better organize all the test cases, you may want to replicate all the package structures of the application. for example, if you create a test case class for edu.scranton.yummynoodlebar.web.model.Customer.java, you may want to create a test class as src/test/ edu.scranton.yummynoodlebar.web.model.CustomerTest.java
- When a test class is created, a setup() method is created for you. It is executed before each test case is started. Each test case is totally independent and isolated from other test cases. You want to keep it that way.
- You want to test all getters and setters of every class. It may seem to be tedious. Yes, you may feel that way the first time. However, remember, you only need to do this only once and then you automatically test the class as many times as needed or you wish. For each instance attribute, you can test its initial value when an instance is created, its getter and setter in one test case as shown below.
public class CustomerTest {
private Customer customer;
@Before
public void setUp() throws Exception {
customer = new Customer();
}
@Test
public void testGetAndSetFirstName() {
assertNull("First name should be null", customer.getFirstName());
String expected = "John";
customer.setFirstName(expected);
assertEquals("First name should be " + expected, customer.getFirstName(),
expected);
}
- After you compose the class, you can right-click the edit area of the source code and select “Run As”->”JUnit Test” to run all the test cases in the class.
- You can run all the test classes and all their test cases of the project by right-clicking the project name and select “Run As”->”JUnit Test”.
2. Testing getters/setters of an entity class is the easiest one in testing. How do you test class A while A calls methods of class B, and B is not implemented yet? We will introduce how JUnit supports stubs using the library of Mockito.
- To use Mockito, we need to include the following into the <-- Test--> section of pom.xml
dependency
<groupIdorg.mockito</groupId
<artifactIdmockito-all</artifactId
<version>1.8.4</version>
<scope>test</scope>
</dependency>
The <scope>test</scope> tells maven this dependency is only for testing. It is not needed for production or runtime.
- Suppose that we have a class called HomeController which calls getAllStudents() of StudentService class. You can image that StudentService would normally get all the students from a database. We want to unit test HomeController’s getAllStudentsAsArray() method. We will use Mockito to stub the StudentService class so we can test HomeController without implementing StudentService.
The Mockito.Mock(StudentService.class) returns a mock of StudentService and Mockito.when(…).thenReturn(…) stubs the getAllStudents() method of StudentService and returns the list of two students.
public class HomeControllerTest {
private StudentService studentService;
private HomeController homeController;
private List<Student> studentList;
private Student student1;
private Student student2;
@Before
public void setUp() throws Exception {
studentList = new ArrayList<Student>();
student1 = new Student("John Smith", "CMPS");
student2 = new Student("Mary Doe", "MATH");
studentList.add(student1);
studentList.add(student2)
studentService = Mockito.mock(StudentService.class);
Mockito.when(studentService.getAllStudents()).thenReturn(studentList);
homeController = new HomeController();
homeController.setStudentService(studentService);
}
@Test
public void testGetAllStudentsAsArray() {
assertNotNull("student not be null", homeController.getAllStudentsAsArray());
assertEquals("thers should be " + studentList.size() + " students",
homeController.getAllStudentsAsArray().size(),
studentList.size());
assertTrue"first student name should be John Smith",
compare(homeController.getAllStudentsAsArray(), student1);
assertTrue("second student name should be Mary Doe",
compare(homeController.getAllStudentsAsArray().get(1), student2);
}
Private Boolean compare(Student student1, Student student2) {
// returns true if student1 and student2 are the same in content
// returns false otherwise
}
}
3. In Spring MVC projects, controllers are called by the front controller, DispatcherServlet, and are passed with the HTTP request which normally contains data entered by the user. How to test those controllers? If you call the methods of those controllers directly, it would be really hard to program the needed HTTP requests. Spring Framework offers a test environment where you can create the application context and have the request go through the front controller. This test environment includes a class called MockMVC which can be used to get the application context, submit HTTP requests, and receive the responses from those controllers.
- Spring Framework does not support MockMVC until 3.2. Thus, we want to use latest version of Spring which is 4.0.6.RELEASE (from 3.1.1, which was included in our downloaded STS). we modify the Spring version in the pom.xml as follows:
properties
<java-version>1.6</java-version>
<org.springframework-version>4.0.6.RELEASE</org.springframework-version>
org.aspectj-version>1.6.10</org.aspectj-version>
<org.slf4j-version>1.6.6</org.slf4j-version>
</properties>
Once you have used 4.0.6.RELEASE once in your workspace, all new Spring MVC projects will use it automatically as default.
- MockMVC also requires that javax.servlet-api to be at least 3.1.0 from (2.1.1?). To include this version of the library, you need to modify the following entry in pom.xml.
<dependency
<groupIdjavax.servlet</groupId
<artifactIdjavax.servlet-api</artifactId
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
- The last dependency you need to include for MockMVC is Spring Test library. Ad d the following to the <!-- Test --> section of pon.xml
dependency
groupIdorg.springframework</groupId
artifactIdspring-test</artifactId
version>${org.springframework-version}</version>
scope>test</scope>
</dependency>.
- For our YummyNoodleBar sample application, w need to configure the dataSource for testing since the CustomerController needs CustomerService which in turns needs CustomerDaoImpl to access the customer table in the database. The database configuration for testing is located in file, src/test/source/WebTest-context.xml, and it should contain the following:.
<!-- Enables the Spring MVC @Controller programming model -->
<annotation-driven />
context:component-scan base-package="edu.scranton.yummynoodlebar" />
<!-- Initialization for data source -->
beans:bean id="dataSource"
class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<beans:property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<beans:property name="url" value="jdbc:mysql://134.198.xxx.xx/yummynoodlebar"/>
<beans:property name="username" value="xxx"/>
<beans:property name="password" value="xxx"/>
</beans:bean
- Create a test class for the HomeController and then add the following three lines right before the class declaration. @ContextConfiguration (locations="classpath:WebTest-context.xml") declares the context for testing.
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration (locations="classpath:WebTest-context.xml")
@WebAppConfiguration
public class HomeControllerTest {
- Declare a private member of MockMvc.
@Autowired
private WebApplicationContext ctx;
private MockMvc mockMvc;
- In setup(), create an instance of MocMvc through MockMvcBuilders. .
@Before
public void setUp() throws Exception {
this.mockMvc = MockMvcBuilders.webAppContextSetup(ctx).build();
}
- Now you can do testing as shown below.
@Test
public void testGetCurrentMenu() throws Exception {
mockMvc.perform(
get("/").accept(MediaType.TEXT_PLAIN))
.andDo(print())
.andExpect(status().isOk())
.andExpect(content().string(" Pizza Hoagie"));
}
- To test the content of a view (XYZ.jsp file), we can include Hamcrest library for declarative matching rules. Include the following in pom.xml dependencies
dependency
groupIdorg.hamcrest</groupId
artifactIdhamcrest-all</artifactId
version>1.3</version>
</dependency>
Now we can test what is in the view returned from the controller like the following:
@Test
public void testDisplayMenu() throws Exception {
mockMvc.perform(get("/menu"))
.andDo(print())
.andExpect(status().isOk())
.andExpect(view().name("displayMenu"))
.andExpect(model().attribute("menu", hasSize(2)))
.andExpect(model().attribute("menu", hasItem(
allOf(
hasProperty("id", is("123")),
hasProperty("name", is("Pizza")),
hasProperty("minutesToPrepare", is(10))
)
)
))
.andExpect(model().attribute("menu", hasItem(
allOf(
hasProperty("id", is("234")),
hasProperty("name", is("Hoagie")),
hasProperty("minutesToPrepare", is(10))))));
}
- The above example tested if the controller returns the expected response. The example below shows how to mimic form input data and then test if the response is correct. The example here is a customer input form using URL “/customer” and POST. Then we expect the new customer is returned in a confirmation page. .
public void testProcessSubmit() throws Exception {
mockMvc.perform(post("/customer")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.param("firstName", "john2")
.param("lastName", "smith2")
.param("address", "Somewhere in scranton")
.param("sex", "M")
.param("type", "STAFF")
.param("password", "123456")
.param("confirmPassword", "12345")
.sessionAttr("customer", new WebCustomer()))
.andDo(print())
.andExpect(status().isOk())
.andExpect(view().name("CustomerConfirmation"))
.andExpect(forwardedUrl("CustomerConfirmation"))
.andExpect(model().attribute("customer", hasProperty("firstName", is("john2"))))
.andExpect(model().attribute("customer", hasProperty("lastName", is("smith2"))));
}
More information and function of Hamcrest can be found at https://code.google.com/p/hamcrest/wiki/Tutorial
NOTE: in your pom.xml file, hamcrest should be listed before mockito. The unofficial reason is that mockito implements some of the hamcrest classes and if they are loaded first, then hamcrest would not be loaded again. Not all of the methods defined in hamcrest are in mockito’s hamcrest. So you would receive “un-defined methods” exceptions.
- The teardown() method (annotated with @After) in JUnit is executed after each test case is completed. We can use this function to remove any effect left over by each test case. For example, the CustomerController.processSubmit() inserts a customer into the database. After testing the function, we don’t want or we should not leave the record in the database. To delete the record, we override the tearDown() method.
For YummyNoodleBar, in order to be able to delete the new customer inserted by processSubmit(), we need to know the unique id assigned to the customer. So we declare a private member, webCustomer, in CustomerControllerTest and the testProcessSubmit() get the customer (of WebCustomer) from the response (CustomerConfirmation.jsp) and assign it to webCustomer. In tearDown(), we call customerService’s deleteCustomer() to delete the new customer only if webCustomer is not null. Now testProcessSubmit() inserts a new customer and tearDown() deletes it when the test case is completed, no side-effect left behind from the test case.
public class CustomerControllerTest {
@Autowired
private CustomerService customerService;
private WebCustomer webCustomer = null;
@Test
public void testProcessSubmit() throws Exception {
ResultActions resultActions = mockMvc.perform(
post("/customer")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.param("firstName", "john4")
.param("lastName", "smith4")
.param("address", "Somewhere in scranton")
.param("sex", "M").param("type", "STAFF")
.param("password", "123456")
.param("hobbies", new String[] {"STAFF", "STUDENT"})
.param("confirmPassword", "12345")
.sessionAttr("customer", new WebCustomer()));
// we need to access the customer just inserted into the database
// so in tearDown() we can delete the same customer to restore the DB.
MvcResult mvcResult = resultActions.andReturn();
ModelAndView modelAndView = mvcResult.getModelAndView();
webCustomer = (WebCustomer) modelAndView.getModel().get("customer");
@After
public void tearDown() {
// we only want to delete the new customer when a new customer was
// inserted into the database. Test cases like testInitForm() does
// not insert a new customer, then webCustomer would still be null.
if (webCustomer != null) {
logger.info("id of the customer to be deleted " + webCustomer.getId());
Customer customer = CustomerUtils.convertWebCustomerToCustomer(webCustomer);
customerService.deleteCustomer(customer);
}
}