用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会被去除

 

posted @ 2017-01-23 15:06  明-Ming  阅读(1759)  评论(0编辑  收藏  举报