首页 > 代码库 > 用Mockito测试SpringMVC+Hibernate

用Mockito测试SpringMVC+Hibernate

用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;    }}
View Code

技术分享

右击该测试类,得到结果如下:

PASSED: deleteEmployeePASSED: editEmployeePASSED: listEmployeesPASSED: newEmployeePASSED: saveEmployeeWithSuccessPASSED: saveEmployeeWithValidationErrorPASSED: saveEmployeeWithValidationErrorNonUniqueSSNPASSED: updateEmployeeWithSuccessPASSED: updateEmployeeWithValidationErrorPASSED: 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;    }    }
View Code

右击该测试类,得到结果如下:

PASSED: deleteEmployeeBySsnPASSED: findAllEmployeesPASSED: findByIdPASSED: findEmployeeBySsnPASSED: isEmployeeSsnUniquePASSED: saveEmployeePASSED: 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;	}}
View Code

技术分享

解读:

  • 上面的类与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;}
View Code

解读:

  • 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;    }}
View Code

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: deleteEmployeeByInvalidSsnPASSED: deleteEmployeeBySsnPASSED: findAllEmployeesPASSED: findByIdPASSED: findEmployeeBySsnPASSED: 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会被去除

 

用Mockito测试SpringMVC+Hibernate