zoukankan      html  css  js  c++  java
  • 用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会被去除

  • 相关阅读:
    LeetCode(287)Find the Duplicate Number
    LeetCode(290) Word Pattern
    LeetCode(205)Isomorphic Strings
    LeetCode(201) Bitwise AND of Numbers Range
    LeetCode(200) Number of Islands
    LeetCode(220) Contains Duplicate III
    LeetCode(219) Contains Duplicate II
    命令行执行Qt程序
    LeetCode(228) Summary Ranges
    redis 的安装和使用记录
  • 原文地址:https://www.cnblogs.com/Ming8006/p/6297822.html
Copyright © 2011-2022 走看看