>> 模拟利器Mockito:
Mockito使用起来非常简单,学习成本低,而且有非常简单的API,测试代码可读性很高。官网地址:http://www.mockito.org/
使用实例:
public class MockitoSampleTest {
//模拟接口
UserService mockUserService = mock(UserService.class);
//模拟实现类
UserServiceImpl mockServiceImpl = mock(UserServiceImpl.class);
//基于注解模拟类
@Mock User mockUser;
@Before
public void initMocks() {
//初始化当前测试类所有@Mock注释模拟对象
MockitoAnnotations.initMocks(this);
}
@Test
//模拟接口UserService测试
public void testMockInterface() {
//对方法设定返回值
when(mockUserService.findUserByUserName("tom")).thenReturn(
new User("tom", "1234"));
doReturn(true).when(mockUserService).hasMatchUser("tom", "1234");
//对void方法进行方法预期设定
User u = new User("John", "1234");
doNothing().when(mockUserService).registerUser(u);
//执行方法调用
User user = mockUserService.findUserByUserName("tom");
boolean isMatch = mockUserService.hasMatchUser("tom","1234");
mockUserService.registerUser(u);
assertNotNull(user);
assertEquals(user.getUserName(), "tom");
assertEquals(isMatch, true);
//验证交互行为
verify(mockUserService).findUserByUserName("tom");
//验证方法只调用一次
verify(mockUserService, times(1)).findUserByUserName("tom");
//验证方法至少调用一次
verify(mockUserService, atLeastOnce()).findUserByUserName("tom");
verify(mockUserService, atLeast(1)).findUserByUserName("tom");
//验证方法至多调用一次
verify(mockUserService, atMost(1)).findUserByUserName("tom");
verify(mockUserService).hasMatchUser("tom", "1234");
verify(mockUserService).registerUser(u);
}
}
>> 测试整合之王Unitils:
Unitils测试框架目的是让单元测试变的更加容易和可维护。Unitils构建在DbUnit与EasyMock项目之上并与JUnit和TestNG相结合。支持数据库测试,支持利用Mock对象进行测试并提供与spring和Hibernate相集成。Unitils设计成以一种高度可配置和松耦合方式添加这些服务到单元测试中。
* unitils-core:核心内核包
* unitils-database:维护测试数据库及连接池
* unitils-DbUnit:使用DbUnit管理测试数据
* unitils-easymock:支持创建Mock和宽松的反射参数匹配
* unitils-inject:支持在一个对象中注入另一个对象
* unitils-mock:整个各种mock,在Mock使用语法上进行简化
* unitils-orm:支持Hibernate、JPA的配置和自动数据库映射检查
* unitils-spring:支持加载spring的上下文配置,并检索和Spring Bean注入
集成spring实例:
public class UserServiceTest extends UnitilsJUnit4 {
@SpringApplicationContext({"baobaotao-service.xml", "baobaotao-dao.xml"})
private ApplicationContext applicationContext;
@SpringBean("userService")
private UserService userService;
@Test
public void testLoadSpringBean(){
assertNotNull(applicationContext);
assertNotNull(userService.findUserByUserName("tom"));
}
}
集成Dbunit实例:
Dbunit是一个基于JUnit扩展的数据库测试框架,通过使用用户自定义的数据集以及相关操作使得数据库处于一种可知状态,从而使测试自动化、可重复和相对独立。随着Unitils的出现,将spring、hibernate、Dbunit等整合在一起,使得测试DAO层单元测试变得非常简单。
单元测试最重要的特征是独立性和可重复性。对于Service层,可以通过Mockito底层对象和上层对象来获取这种独立性和可重复性。但是DAO层因为是和数据库打交道,其单元测试依赖于数据库中的数据,要实现DAO层单元测试可重复性需要对每次因单元测试引起的数据库中的数据变化进行还原,相当麻烦。
Unitils的出现改变了难测试DAO的局面,它将SpringModule、DatabaseModule、DbUnitModule整合在一起,使得DAO的单元测试变得非常容易。
下面是演示通过excel文件测试DAO层:
Excel数据集工厂
public class MultiSchemaXlsDataSetFactory implements DataSetFactory {
protected String defaultSchemaName;
//初始化
public void init(Properties configuration, String defaultSchemaName) {
this.defaultSchemaName = defaultSchemaName;
}
//创建数据集
public MultiSchemaDataSet createDataSet(File... dataSetFiles) {
try {
MultiSchemaXlsDataSetReader xlsDataSetReader = new MultiSchemaXlsDataSetReader(
defaultSchemaName);
return xlsDataSetReader.readDataSetXls(dataSetFiles);
} catch (Exception e) {
throw new UnitilsException("创建数据集失败: "
+ Arrays.toString(dataSetFiles), e);
}
}
// 获取数据集文件的扩展名
public String getDataSetFileExtension() {
return "xls";
}
}
Excel数据集读取器:
//EXCEL数据集读取器
public class MultiSchemaXlsDataSetReader {
private String defaultSchemaName;
public MultiSchemaXlsDataSetReader(String defaultSchemaName) {
this.defaultSchemaName = defaultSchemaName;
}
public MultiSchemaDataSet readDataSetXls(File... dataSetFiles) {
try {
Map> tableMap = getTables(dataSetFiles);
MultiSchemaDataSet dataSets = new MultiSchemaDataSet();
for (Entry> entry : tableMap.entrySet()) {
List tables = entry.getValue();
try {
DefaultDataSet ds = new DefaultDataSet(tables
.toArray(new ITable[]{}));
dataSets.setDataSetForSchema(entry.getKey(), ds);
} catch (AmbiguousTableNameException e) {
throw new UnitilsException("构造DataSet失败!", e);
}
}
return dataSets;
} catch (Exception e) {
throw new UnitilsException("解析EXCEL文件出错:", e);
}
}
private Map> getTables(File... dataSetFiles) {
Pattern pattern = Pattern.compile("\\.");
Map> tableMap = new HashMap>();
// 需要根据schema把Table重新组合一下
try {
for (File file : dataSetFiles) {
IDataSet dataSet = new XlsDataSet(new FileInputStream(file));
String[] tableNames = dataSet.getTableNames();
for (String each : tableNames) {
// 这个实际上不是schema, 是对应的spring的datasouceId
String schema = null;
String tableName;
String[] temp = pattern.split(each);
if (temp.length == 2) {
schema = temp[0];
tableName = temp[1];
} else {
schema = this.defaultSchemaName;
tableName = each;
}
ITable table = dataSet.getTable(each);
if (!tableMap.containsKey(schema)) {
tableMap.put(schema, new ArrayList());
}
tableMap.get(schema).add(new XlsTable(tableName, table));
}
}
} catch (Exception e) {
throw new UnitilsException("创建数据集失败: "
+ Arrays.toString(dataSetFiles), e);
}
return tableMap;
}
//构造XslTable表
class XlsTable extends AbstractTable {
private ITable delegate;
private String tableName;
public XlsTable(String tableName, ITable table) {
this.delegate = table;
this.tableName = tableName;
}
public int getRowCount() {
return delegate.getRowCount();
}
public ITableMetaData getTableMetaData() {
ITableMetaData meta = delegate.getTableMetaData();
try {
return new DefaultTableMetaData(tableName, meta.getColumns(),
meta.getPrimaryKeys());
} catch (DataSetException e) {
throw new UnitilsException("Don't get the meta info from "
+ meta, e);
}
}
public Object getValue(int row, String column) throws DataSetException {
Object delta = delegate.getValue(row, column);
if (delta instanceof String) {
if (StringUtils.isEmpty((String) delta)) {
return null;
}
}
return delta;
}
}
}
另外还有一个工具类,用于从excel里面初始化测试数据变成一个pojo类:
/**
* 从EXCEL数据集文件创建Bean
*/
public class XlsDataSetBeanFactory {
//从DbUnit的EXCEL数据集文件创建多个bean
public static List createBeans(Class testClass, String file, String tableName,
Class clazz) throws Exception {
BeanUtilsBean beanUtils = createBeanUtils();
List
接下来配置下unitils,使用我们自定义的数据集工厂类:
DbUnitModule.DataSet.factory.default=sample.unitils.dataset.excel.MultiSchemaXlsDataSetFactory
DbUnitModule.ExpectedDataSet.factory.default=sample.unitils.dataset.excel.MultiSchemaXlsDataSetFactory
DbUnitModule.DataSet.factory.default指的是配置数据集工厂类,可使用@DataSet注解标注数据集,而DbUnitModule.ExpectedDataSet.factory.default指的是验证数据集工厂类,可使用@ExpectedDataSet注解标注验证数据。
用户测试DAO:
@SpringApplicationContext( {"baobaotao-dao.xml" })
public class UserDaoTest extends UnitilsJUnit4 {
@SpringBean("jdbcUserDao")
private UserDao userDao;
@Test
@DataSet("BaobaoTao.Users.xls")//准备数据
public void findUserByUserName() {
User user = userDao.findUserByUserName("tony");
assertNull("不存在用户名为tony的用户!", user);
user = userDao.findUserByUserName("jan");
assertNotNull("Jan用户存在!", user);
assertEquals("jan", user.getUserName());
assertEquals("123456",user.getPassword());
assertEquals(10,user.getCredits());
}
// 验证数据库保存的正确性
@Test
@ExpectedDataSet("BaobaoTao.ExpectedSaveUser.xls")// 准备验证数据
public void saveUser()throws Exception {
/**
硬编码创建测试实体
User u = new User();
u.setUserId(1);
u.setUserName("tom");
u.setPassword("123456");
u.setLastVisit(getDate("2011-06-06 08:00:00","yyyy-MM-dd HH:mm:ss"));
u.setCredits(30);
u.setLastIp("127.0.0.1");
**/
//通过XlsDataSetBeanFactory数据集绑定工厂创建测试实体
User u = XlsDataSetBeanFactory.createBean("BaobaoTao.SaveUser.xls", "t_user", User.class);
userDao.save(u); //执行用户信息更新操作
}
//验证数据库保存的正确性
@Test
@ExpectedDataSet("BaobaoTao.ExpectedSaveUsers.xls")// 准备验证数据
public void saveUsers()throws Exception {
List users = XlsDataSetBeanFactory.createBeans("BaobaoTao.SaveUsers.xls", "t_user", User.class);
for(User u:users){
userDao.save(u);
}
}
}
>> unitils配置文件unitils.properties说明:
# 这个是整个项目级别的公共配置,如果每个用户也就是开发者去测试的时候可以自己定义unitils-local.properties文件
# 启用unitils所需模块
unitils.modules=database,dbunit,hibernate,spring,
# 自定义扩展模块
#unitils.module.dbunit.className=sample.unitils.module.CustomExtModule
# 配置数据库连接
database.driverClassName=com.mysql.jdbc.Driver
database.url=jdbc:mysql://localhost:3306/junit?useUnicode=true&characterEncoding=UTF-8
database.dialect=mysql
database.username=root
database.password=123456
# 配置数据库维护策略
updateDataBaseSchema.enabled=true
# 配置数据库表创建策略
dbMaintainer.autoCreateExecutedScriptsTable=true
dbMaintainer.script.locations=D:/workspace/spring3/spring3-core/ch16_uint_test/src/main/resources/dbscripts
# 配置数据集加载策略
DbUnitModule.DataSet.loadStrategy.default=org.unitils.dbunit.datasetloadstrategy.impl.CleanInsertLoadStrategy
# 配置数据集加载工厂
DbUnitModule.DataSet.factory.default=sample.unitils.dataset.excel.MultiSchemaXlsDataSetFactory
DbUnitModule.ExpectedDataSet.factory.default=sample.unitils.dataset.excel.MultiSchemaXlsDataSetFactory
# 配置事务策略
DatabaseModule.Transactional.value.default=commit
# 配置数据集结构模式XSD生成路径
dataSetStructureGenerator.xsd.dirName=resources/xsd
* 配置数据集加载策略:
默认的数据集加载机制采用先清理后插入的策略,也就是数据在被写入数据库的时候先删除数据集中有对应表的数据,然后将数据集中的数据写入数据库。这个加载策略是可以配置的:
CleanInsertLoadStrategy:先删除dataSet中有关的数据,然后再插入数据
InsertLoadStrategy:只插入数据
RefreshLoadStrategy:有同样key的数据更新,没有的插入
UpdateLoadStrategy:有同样key的数据更新,没有的吧不做任何操作
* 配置事务策略:
默认事务管理是disabled掉的,可以设置DatabaseModule.Transactional.value.default,有几个值:commit、rollback、disabled
>> 测试service层:
@SpringApplicationContext( {"baobaotao-service.xml", "baobaotao-dao.xml" })
public class UserServiceTest extends UnitilsJUnit4 {
@SpringBean("userService")
private UserService userService;
private UserDao userDao;
private LoginLogDao loginLogDao;
@Before
public void init(){
userDao = mock(UserDao.class);
loginLogDao = mock(LoginLogDao.class);
}
@Test
public void hasMatchUser() {
User user = new User();
user.setUserName("tom");
user.setPassword("1234");
user.setCredits(100);
doReturn(1).when(userDao).getMatchCount("tom", "1234");
UserServiceImpl userService = new UserServiceImpl();
ReflectionTestUtils.setField(userService, "userDao", userDao);
boolean isMatch = userService.hasMatchUser("tom", "1234");
assertThat(isMatch,is(true));
verify(userDao,times(1)).getMatchCount("tom", "1234");
}
//@Test
public void findUserByUserName() {
User user = new User();
user.setUserName("tom");
user.setPassword("1234");
user.setCredits(100);
doReturn(user).when(userDao).findUserByUserName("tom");
UserServiceImpl userService = new UserServiceImpl();
ReflectionTestUtils.setField(userService, "userDao", userDao);
User u = userService.findUserByUserName("tom");
assertNotNull(u);
assertThat(u.getUserName(),equalTo(user.getUserName()));
verify(userDao,times(1)).findUserByUserName("tom");
}
@Test
@DataSet("BaobaoTao.SaveUsers.xls")// 准备验证数据
public void loginSuccess() {
User user = userService.findUserByUserName("tom");
Date now = new Date();
user.setLastVisit(now);
userService.loginSuccess(user);
User u = userService.findUserByUserName("tom");
assertThat(u.getCredits(),is(105));
}
}
从上面为用户服务UserService编写的两个测试方法来看,对service层的测试,我们既可以采用JUnit+Unitils+Mockito组合,运用Mockito强大的模块能力,完成service层独立性的单元测试,也就是findUserByUserName()测试方法;也可以采用JUnit+Unitils+Dbunit组合,运用Dbunit强大的数据库维护能力(比如自动回滚,确保每次测试数据的纯洁性)以及Unitils通过DataSet利用Excel、Xml等数据产生方式,完成service+DAO层的集成测试。
>> 测试web层:
spring在org.springframework.mock包中为一些依赖于容器的接口提供了模拟类,这样用户就可以在不启动容器(这里指的是Java EE容器、Servlet容器、Portlet容器等,也就是tomcat、jboss等)的情况下执行单元测试,提高测试效率
* org.springframework.mock.jndi:为JNDI SPI接口提供模拟类以便脱离java EE容器进行测试
* org.springframework.mock.web:为Servlet API接口如HttpServletRequest提供模拟类,以便脱离Servlet容器测试
* org.springframework.mock.web.portlet:为Portlet API接口如PortletRequest提供模拟类,以便可以脱离Portlet容器测试
@SpringApplicationContext( { "classpath:applicationContext.xml",
"file:D:/workspace/spring3/spring3-core/ch16_uint_test/src/main/webapp/WEB-INF/baobaotao-servlet.xml" })
public class LoginControllerTest extends UnitilsJUnit4 {
// ① 从Spring容器中加载AnnotationMethodHandlerAdapter
@SpringBeanByType
private AnnotationMethodHandlerAdapter handlerAdapter;
// ② 从Spring容器中加载LoginController
@SpringBeanByType
private LoginController controller;
@SpringBeanByType
private RestTemplate restTemplate;
// ③ 声明Request与Response模拟对象
private MockHttpServletRequest request;
private MockHttpServletResponse response;
// ④ 执行测试前先初始模拟对象
@Before
public void before() {
request = new MockHttpServletRequest();
request.setCharacterEncoding("UTF-8");
response = new MockHttpServletResponse();
}
@Test
public void loginCheck() {
MultiValueMap map = new LinkedMultiValueMap();
map.add("userName", "john");
map.add("password", "1234");
String result = restTemplate.postForObject(
"http://localhost:8080/ch16/loginCheck.html", map, String.class);
assertNotNull(result);
assertThat(result, containsString("用户名或密码错误"));
map.clear();
map.add("userName", "tom");
map.add("password", "1234");
result = restTemplate.postForObject(
"http://localhost:8080/ch16/loginCheck.html", map, String.class);
System.out.println(result);
assertNotNull(result);
assertThat(result, containsString("tom,欢迎您进入宝宝淘论坛"));
}
// ⑤ 测试LoginController#loginCheck()方法
@Test
public void loginCheckByMock() throws Exception {
request.setRequestURI("/loginCheck.html");
request.addParameter("userName", "tom"); // ⑥ 设置请求URL及参数
request.addParameter("password", "123456");
// ⑦ 向控制发起请求 ” /loginCheck.html”
ModelAndView mav = handlerAdapter.handle(request, response, controller);
User user = (User) request.getSession().getAttribute("user");
assertNotNull(mav);
assertEquals(mav.getViewName(), "main");
assertNotNull(user);
assertThat(user.getUserName(), equalTo("tom"));// ⑧ 验证返回结果
assertThat(user.getCredits(), greaterThan(5));
}
}
上面的测试方法中,loginCheck是通过启动web应用服务器后,利用RestTemplate编写访问客户端进行集成测试,而loginCheckByMock是在不启动web服务器的情况下,直接对controller进行的单元测试,因为Controller用到了Service,而service用到dao,dao访问了数据库,其实也可以利用mockito模拟service层调用,仅仅对controller进行纯粹的单元测试。
>> 使用Selenium模拟真实用户的表单提交以及不同浏览器兼容
Selenium是ThoughtWorks公司开发的基于Web应用的测试工具,直接运行在浏览器中,模拟用户操作,包括Selenium-IDE、Selenium-core、Selenium-rc三部分
下面是测试用的代码示例:
登录页面login.jsp:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
宝宝淘论坛登录
" method= "post">
用户名:
密 码:
主页面main.jsp:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
宝宝淘论坛
${user.userName},欢迎您进入宝宝淘论坛,您当前积分为${user.credits};
然后就是测试LoginController的测试类:
public class LoginControllerTest {
//声明WebDriver
WebDriver driver = null;
@Before
public void init() {
driver = new HtmlUnitDriver(); //IE
}
@Test
public void loginCheck(){
//完全装载页面后将控制返回给测试脚本
//navigate().to()和get()功能完全一样。
driver.get("http://localhost/chapter16/index.html");
//(XPATH返回第一个匹配到的元素,如果没有匹配到,抛出NoSuchElementException)
//element = driver.findElement(By.xpath( "//input[@id=’xxx’]" ));
WebElement userName = driver.findElement(By.name("userName"));
WebElement password = driver.findElement(By.name("password"));
//任何页面元素都可以调用sendKeys,
userName.sendKeys("tom");
password.sendKeys("1234");
//提交表单
driver.findElement(By.id( "loginBtn" )).click();
//driver.findElement(By.id( "submit" )).submit(); 要求element必须在表单中,否则抛出NoSuchElementException
//验证返回的主页面 main.jsp
assertThat(driver.getTitle(), equalTo("宝宝淘论坛"));
assertThat(driver.getPageSource(), containsString("tom"));
WebElement body = driver.findElement(By.xpath( "//body" ));
assertThat(body.getText(), containsString("tom,欢迎您进入宝宝淘论坛"));
}
}
总结web层测试:
从以上对LoginController编写的三个测试方法可以看出,对Web层的测试,
* 我们既可以采用JUnit+Unitils+Spring Mock组合,运用Spring Mock模拟依赖于容器的接口实例,如HttpServletRequest、ServletContext等,完成Web层中Controller独立性测试
* 也可以采用JUnit+Unitils+Spring RestTemplate,完成Web层继承测试
* 还可以采用JUnit+Selenium组合,来模拟真实用户的操作以及跨浏览器兼容测试
本人博客已搬家,新地址为:http://yidao620c.github.io/