用Mockito测试SpringMVC+Hibernate
译自:Spring 4 MVC+Hibernate 4+MySQL+Maven integration + Testing example using annotations
2017-01-19
1 目录结构
2 pom.xml
3 Testing Controller Layer
3.1 com.websystique.springmvc.controller.AppControllerTest
4 Testing Service Layer
4.1 com.websystique.springmvc.service.EmployeeServiceImplTest
5 Testing Data Layer
5.1 com.websystique.springmvc.configuration.HibernateTestConfiguration
5.2 com.websystique.springmvc.dao.EntityDaoImplTest
5.3 com.websystique.springmvc.dao.EmployeeDaoImplTest
5.4 src/test/resources/Employee.xml
源代码 : SpringHibernateExample.zip
1 目录结构
2 pom.xml
与 被测项目 Spring 4 MVC+Hibernate 4+MySQL+Maven使用注解集成实例中pom.xml 一样。
其中,
- Spring-test : 在测试类中使用 spring-test annotations
- TestNG : 使用testNG作为测试框架
- Mockito : 使用mockito模拟外部依赖, 比如当测试service时mock dao,关于mockito,请参考Mockito教程
- DBUnit : 使用DBUnit管理数据,当测试data/dao层时
- H2 Database : 对数据库层测试,与其说是单元测试不如说是集成测试,使用H2 Database对数据库层进行测试
3 Testing Controller Layer
3.1 com.websystique.springmvc.controller.AppControllerTest
package com.websystique.springmvc.controller; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.anyInt; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.when; import static org.mockito.Mockito.verify; import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; import org.joda.time.LocalDate; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.Spy; import static org.mockito.Mockito.atLeastOnce; import org.springframework.context.MessageSource; import org.springframework.ui.ModelMap; import org.springframework.validation.BindingResult; import org.testng.Assert; import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import com.websystique.springmvc.model.Employee; import com.websystique.springmvc.service.EmployeeService; public class AppControllerTest { @Mock EmployeeService service; @Mock MessageSource message; @InjectMocks AppController appController; @Spy List<Employee> employees = new ArrayList<Employee>(); @Spy ModelMap model; @Mock BindingResult result; @BeforeClass public void setUp(){ MockitoAnnotations.initMocks(this); employees = getEmployeeList(); } @Test public void listEmployees(){ when(service.findAllEmployees()).thenReturn(employees); Assert.assertEquals(appController.listEmployees(model), "allemployees"); Assert.assertEquals(model.get("employees"), employees); verify(service, atLeastOnce()).findAllEmployees(); } @Test public void newEmployee(){ Assert.assertEquals(appController.newEmployee(model), "registration"); Assert.assertNotNull(model.get("employee")); Assert.assertFalse((Boolean)model.get("edit")); Assert.assertEquals(((Employee)model.get("employee")).getId(), 0); } @Test public void saveEmployeeWithValidationError(){ when(result.hasErrors()).thenReturn(true); doNothing().when(service).saveEmployee(any(Employee.class)); Assert.assertEquals(appController.saveEmployee(employees.get(0), result, model), "registration"); } @Test public void saveEmployeeWithValidationErrorNonUniqueSSN(){ when(result.hasErrors()).thenReturn(false); when(service.isEmployeeSsnUnique(anyInt(), anyString())).thenReturn(false); Assert.assertEquals(appController.saveEmployee(employees.get(0), result, model), "registration"); } @Test public void saveEmployeeWithSuccess(){ when(result.hasErrors()).thenReturn(false); when(service.isEmployeeSsnUnique(anyInt(), anyString())).thenReturn(true); doNothing().when(service).saveEmployee(any(Employee.class)); Assert.assertEquals(appController.saveEmployee(employees.get(0), result, model), "success"); Assert.assertEquals(model.get("success"), "Employee Axel registered successfully"); } @Test public void editEmployee(){ Employee emp = employees.get(0); when(service.findEmployeeBySsn(anyString())).thenReturn(emp); Assert.assertEquals(appController.editEmployee(anyString(), model), "registration"); Assert.assertNotNull(model.get("employee")); Assert.assertTrue((Boolean)model.get("edit")); Assert.assertEquals(((Employee)model.get("employee")).getId(), 1); } @Test public void updateEmployeeWithValidationError(){ when(result.hasErrors()).thenReturn(true); doNothing().when(service).updateEmployee(any(Employee.class)); Assert.assertEquals(appController.updateEmployee(employees.get(0), result, model,""), "registration"); } @Test public void updateEmployeeWithValidationErrorNonUniqueSSN(){ when(result.hasErrors()).thenReturn(false); when(service.isEmployeeSsnUnique(anyInt(), anyString())).thenReturn(false); Assert.assertEquals(appController.updateEmployee(employees.get(0), result, model,""), "registration"); } @Test public void updateEmployeeWithSuccess(){ when(result.hasErrors()).thenReturn(false); when(service.isEmployeeSsnUnique(anyInt(), anyString())).thenReturn(true); doNothing().when(service).updateEmployee(any(Employee.class)); Assert.assertEquals(appController.updateEmployee(employees.get(0), result, model, ""), "success"); Assert.assertEquals(model.get("success"), "Employee Axel updated successfully"); } @Test public void deleteEmployee(){ doNothing().when(service).deleteEmployeeBySsn(anyString()); Assert.assertEquals(appController.deleteEmployee("123"), "redirect:/list"); } public List<Employee> getEmployeeList(){ Employee e1 = new Employee(); e1.setId(1); e1.setName("Axel"); e1.setJoiningDate(new LocalDate()); e1.setSalary(new BigDecimal(10000)); e1.setSsn("XXX111"); Employee e2 = new Employee(); e2.setId(2); e2.setName("Jeremy"); e2.setJoiningDate(new LocalDate()); e2.setSalary(new BigDecimal(20000)); e2.setSsn("XXX222"); employees.add(e1); employees.add(e2); return employees; } }
右击该测试类,得到结果如下:
PASSED: deleteEmployee PASSED: editEmployee PASSED: listEmployees PASSED: newEmployee PASSED: saveEmployeeWithSuccess PASSED: saveEmployeeWithValidationError PASSED: saveEmployeeWithValidationErrorNonUniqueSSN PASSED: updateEmployeeWithSuccess PASSED: updateEmployeeWithValidationError PASSED: updateEmployeeWithValidationErrorNonUniqueSSN =============================================== Default test Tests run: 10, Failures: 0, Skips: 0 ===============================================
解读:
因为被测类AppController依赖EmployeeService , MessageSource, Employee, ModelMap & BindingResult。因此,为了测试AppController,需要提供这些依赖。
@Mock //Mock不是真实的对象,它只是用类型的class创建了一个虚拟对象,并可以设置对象行为 EmployeeService service; @Mock MessageSource message; @InjectMocks //InjectMocks创建这个类的对象并自动将标记@Mock、@Spy等注解的属性值注入到这个中 AppController appController; @Spy //Spy是一个真实的对象,但它可以设置对象行为 List<Employee> employees = new ArrayList<Employee>(); @Spy ModelMap model; @Mock BindingResult result;
其中,when..then 模式用于设置对象行为。
另外,需要加入以下代码:
MockitoAnnotations.initMocks(this); //初始化被注释的[@Mock, @Spy, @Captor, @InjectMocks] 对象
4 Testing Service Layer
4.1 com.websystique.springmvc.service.EmployeeServiceImplTest
package com.websystique.springmvc.service; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.anyInt; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.verify; import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; import static org.mockito.Mockito.when; import org.joda.time.LocalDate; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.Spy; import org.testng.Assert; import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import com.websystique.springmvc.dao.EmployeeDao; import com.websystique.springmvc.model.Employee; public class EmployeeServiceImplTest { @Mock EmployeeDao dao; @InjectMocks EmployeeServiceImpl employeeService; @Spy List<Employee> employees = new ArrayList<Employee>(); @BeforeClass public void setUp(){ MockitoAnnotations.initMocks(this); employees = getEmployeeList(); } @Test public void findById(){ Employee emp = employees.get(0); when(dao.findById(anyInt())).thenReturn(emp); Assert.assertEquals(employeeService.findById(emp.getId()),emp); } @Test public void saveEmployee(){ doNothing().when(dao).saveEmployee(any(Employee.class)); employeeService.saveEmployee(any(Employee.class)); verify(dao, atLeastOnce()).saveEmployee(any(Employee.class)); } @Test public void updateEmployee(){ Employee emp = employees.get(0); when(dao.findById(anyInt())).thenReturn(emp); employeeService.updateEmployee(emp); verify(dao, atLeastOnce()).findById(anyInt()); } @Test public void deleteEmployeeBySsn(){ doNothing().when(dao).deleteEmployeeBySsn(anyString()); employeeService.deleteEmployeeBySsn(anyString()); verify(dao, atLeastOnce()).deleteEmployeeBySsn(anyString()); } @Test public void findAllEmployees(){ when(dao.findAllEmployees()).thenReturn(employees); Assert.assertEquals(employeeService.findAllEmployees(), employees); } @Test public void findEmployeeBySsn(){ Employee emp = employees.get(0); when(dao.findEmployeeBySsn(anyString())).thenReturn(emp); Assert.assertEquals(employeeService.findEmployeeBySsn(anyString()), emp); } @Test public void isEmployeeSsnUnique(){ Employee emp = employees.get(0); when(dao.findEmployeeBySsn(anyString())).thenReturn(emp); Assert.assertEquals(employeeService.isEmployeeSsnUnique(emp.getId(), emp.getSsn()), true); } public List<Employee> getEmployeeList(){ Employee e1 = new Employee(); e1.setId(1); e1.setName("Axel"); e1.setJoiningDate(new LocalDate()); e1.setSalary(new BigDecimal(10000)); e1.setSsn("XXX111"); Employee e2 = new Employee(); e2.setId(2); e2.setName("Jeremy"); e2.setJoiningDate(new LocalDate()); e2.setSalary(new BigDecimal(20000)); e2.setSsn("XXX222"); employees.add(e1); employees.add(e2); return employees; } }
右击该测试类,得到结果如下:
PASSED: deleteEmployeeBySsn PASSED: findAllEmployees PASSED: findById PASSED: findEmployeeBySsn PASSED: isEmployeeSsnUnique PASSED: saveEmployee PASSED: updateEmployee =============================================== Default test Tests run: 7, Failures: 0, Skips: 0 ===============================================
Test Service Layer和Test Control Layer类似,不再详述
5 Testing Data Layer
DAO 或 data Layer测试一直是有争议的话题。我们到底要如何测试?把它当做单元测试的话,就要测试它的每一行代码,这样的话,要mocking所有的外部依赖。但是,我们没有与数据库本身的交互,就没法测data-layer。那么它就变成了集成测试。
通常,我们对DAO Layer做集成测试。这里,我们用in-memory H2 database做集成测试。
5.1 com.websystique.springmvc.configuration.HibernateTestConfiguration
package com.websystique.springmvc.configuration; import java.util.Properties; import javax.sql.DataSource; import org.hibernate.SessionFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; import org.springframework.jdbc.datasource.DriverManagerDataSource; import org.springframework.orm.hibernate4.HibernateTransactionManager; import org.springframework.orm.hibernate4.LocalSessionFactoryBean; import org.springframework.transaction.annotation.EnableTransactionManagement; /* * This class is same as real HibernateConfiguration class in sources. * Only difference is that method dataSource & hibernateProperties * implementations are specific to Hibernate working with H2 database. */ @Configuration @EnableTransactionManagement @ComponentScan({ "com.websystique.springmvc.dao" }) public class HibernateTestConfiguration { @Autowired private Environment environment; @Bean public LocalSessionFactoryBean sessionFactory() { LocalSessionFactoryBean sessionFactory = new LocalSessionFactoryBean(); sessionFactory.setDataSource(dataSource()); sessionFactory.setPackagesToScan(new String[] { "com.websystique.springmvc.model" }); sessionFactory.setHibernateProperties(hibernateProperties()); return sessionFactory; } @Bean(name = "dataSource") public DataSource dataSource() { DriverManagerDataSource dataSource = new DriverManagerDataSource(); dataSource.setDriverClassName("org.h2.Driver"); dataSource.setUrl("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE"); dataSource.setUsername("sa"); dataSource.setPassword(""); return dataSource; } private Properties hibernateProperties() { Properties properties = new Properties(); properties.put("hibernate.dialect", "org.hibernate.dialect.H2Dialect"); properties.put("hibernate.hbm2ddl.auto", "create-drop"); return properties; } @Bean @Autowired public HibernateTransactionManager transactionManager(SessionFactory s) { HibernateTransactionManager txManager = new HibernateTransactionManager(); txManager.setSessionFactory(s); return txManager; } }
解读:
- 上面的类与HibernateConfiguration类非常相似,区别仅在 dataSource() & hibernateProperties()这两个方法的实现。
- 在Sources folder中,它做了几乎同样事情:它用dataSource创建了SessionFacoty,其中,dataSource被配置成可与in-memory database H2一起工作。为了使hibernate与H2一起工作,设置hibernate.dialect为H2Dialect。
- SessionFacoty会被注入到AbstractDao,而后当测试EmployeeDaoImpl类时,EmployeeDaoImpl会使用SessionFacoty。
5.2 com.websystique.springmvc.dao.EntityDaoImplTest
该类是所有测试累的基类
package com.websystique.springmvc.dao; import javax.sql.DataSource; import org.dbunit.database.DatabaseDataSourceConnection; import org.dbunit.database.IDatabaseConnection; import org.dbunit.dataset.IDataSet; import org.dbunit.operation.DatabaseOperation; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.testng.AbstractTransactionalTestNGSpringContextTests; import org.testng.annotations.BeforeMethod; import com.websystique.springmvc.configuration.HibernateTestConfiguration; @ContextConfiguration(classes = { HibernateTestConfiguration.class }) public abstract class EntityDaoImplTest extends AbstractTransactionalTestNGSpringContextTests { @Autowired DataSource dataSource; @BeforeMethod public void setUp() throws Exception { IDatabaseConnection dbConn = new DatabaseDataSourceConnection( dataSource); DatabaseOperation.CLEAN_INSERT.execute(dbConn, getDataSet()); } protected abstract IDataSet getDataSet() throws Exception; }
解读:
- AbstractTransactionalTestNGSpringContextTests在某种程度上可以更JUnit的RunWith等价。这个抽象类集成Spring TestContext support到TestNG environment中。
- 为了在测试中提供数据访问层的支持,它也需要在ApplicationContext中定义datasource和transactionManager。我们已在上面的Configuration类中定义了datasource和transactionManager。
- 由于事物支持,每次测试前一个事物会被默认启动,在每次测试结束后这个事物会被回滚。你可以override这个回滚行为。
- BeforeTest在测试用执行前,我们将使用DBUnit去clean-insert测试数据库[h2]中的数据样例。这样避免各个测试方法之间的影响
- 抽象方法getDataSet会在测试类中实现为了在测试前提供真实的测试数据
5.3 com.websystique.springmvc.dao.EmployeeDaoImplTest
package com.websystique.springmvc.dao; import java.math.BigDecimal; import org.dbunit.dataset.IDataSet; import org.dbunit.dataset.xml.FlatXmlDataSet; import org.joda.time.LocalDate; import org.springframework.beans.factory.annotation.Autowired; import org.testng.Assert; import org.testng.annotations.Test; import com.websystique.springmvc.model.Employee; public class EmployeeDaoImplTest extends EntityDaoImplTest{ @Autowired EmployeeDao employeeDao; @Override protected IDataSet getDataSet() throws Exception{ IDataSet dataSet = new FlatXmlDataSet(this.getClass().getClassLoader().getResourceAsStream("Employee.xml")); return dataSet; } /* In case you need multiple datasets (mapping different tables) and you do prefer to keep them in separate XML's @Override protected IDataSet getDataSet() throws Exception { IDataSet[] datasets = new IDataSet[] { new FlatXmlDataSet(this.getClass().getClassLoader().getResourceAsStream("Employee.xml")), new FlatXmlDataSet(this.getClass().getClassLoader().getResourceAsStream("Benefits.xml")), new FlatXmlDataSet(this.getClass().getClassLoader().getResourceAsStream("Departements.xml")) }; return new CompositeDataSet(datasets); } */ @Test public void findById(){ Assert.assertNotNull(employeeDao.findById(1)); Assert.assertNull(employeeDao.findById(3)); } @Test public void saveEmployee(){ employeeDao.saveEmployee(getSampleEmployee()); Assert.assertEquals(employeeDao.findAllEmployees().size(), 3); } @Test public void deleteEmployeeBySsn(){ employeeDao.deleteEmployeeBySsn("11111"); Assert.assertEquals(employeeDao.findAllEmployees().size(), 1); } @Test public void deleteEmployeeByInvalidSsn(){ employeeDao.deleteEmployeeBySsn("23423"); Assert.assertEquals(employeeDao.findAllEmployees().size(), 2); } @Test public void findAllEmployees(){ Assert.assertEquals(employeeDao.findAllEmployees().size(), 2); } @Test public void findEmployeeBySsn(){ Assert.assertNotNull(employeeDao.findEmployeeBySsn("11111")); Assert.assertNull(employeeDao.findEmployeeBySsn("14545")); } public Employee getSampleEmployee(){ Employee employee = new Employee(); employee.setName("Karen"); employee.setSsn("12345"); employee.setSalary(new BigDecimal(10980)); employee.setJoiningDate(new LocalDate()); return employee; } }
5.4 src/test/resources/Employee.xml
<?xml version="1.0" encoding="UTF-8"?> <dataset> <employee id="1" NAME="SAMY" JOINING_DATE="2014-04-16" SALARY="20000" SSN="11111" /> <employee id="2" NAME="TOMY" JOINING_DATE="2014-05-17" SALARY="23000" SSN="11112" /> </dataset>
右击该测试类,得到结果如下:
PASSED: deleteEmployeeByInvalidSsn PASSED: deleteEmployeeBySsn PASSED: findAllEmployees PASSED: findById PASSED: findEmployeeBySsn PASSED: saveEmployee =============================================== Default test Tests run: 6, Failures: 0, Skips: 0 ===============================================
我们以saveEmployee为例解读下执行过程:
1. 在测试方法运行前,Spring会通过@ContextConfiguration注释的EntityDaoImplTest类加载text context,还会通过AbstractTransactionalTestNGSpringContextTests创建beans实例。这只会发生一次。
2. 在bean实例创建前,Spring会创建SessionFactory Bean,并且SessionFactory Bean会被注入dataSource bean(在HibernateTestConfiguration类中定义),见下面属性设置
properties.put("hibernate.hbm2ddl.auto", "create-drop");
注意:由于hbm2ddl属性,当SessionFactory被创建,与Model类相关的schema会被验证并导出到数据库。这意味着Employee表会在H2数据库中创建。
3. 在测试前,@BeforeMethod注释的方法会被调用,该方法会通知DBUnit连接数据库执行clean-insert,在Employee表插入两个记录(见Employee.xml内容)
4. 现在测试用例saveEmployee将开始执行,在执行开始前,事物将被启动,saveEmployee方法本身将在事物中运行。一旦saveEmployee方法运行完毕,事物会回滚到默认的setup。
5. 测试用例saveEmployee开始执行。它会调用employeeDao.saveEmployee(getSampleEmployee()),被调用者会通过hibernate插入预先定义的Employee到H2 database中。这是关键的一步。在这一个之后,就会有3条记录在H2 database中。
6. 在下一个用例中,@BeforeMethod又会被调用
7. 当所有用例测试完后,session会被关掉,schema会被去除