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

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

    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;
    	}
    }
    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: 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会被去除

  • 相关阅读:
    git 远程仓库 and 分支管理
    oracle 导入导出dmp
    sql if else 用法
    设置session超时的三种方式
    eclipse定制工具栏,修改工具栏
    cxf实例异常
    js 表格插入指定行
    iframe 调用父页面元素
    iframe页面刷新
    frame 和 iframe
  • 原文地址:https://www.cnblogs.com/Ming8006/p/6297822.html
Copyright © 2020-2023  润新知