JUnit相信大家都在用,对于如何使用Junit这里不在赘述。Stub和Mock就是两种协助Junit测试的思想或者策略,它们并不是真实存在的对象,却可以模拟真实对象的状态和交互。
Stub和Mock综述
为何使用Stub和Mock呢?在现实的开发过程中,需要去测试的一个方法并不总是单独存在的,它可能依赖其他方法或者类,比如你要测试service层的逻辑,可能依赖于dao层的数据交互;测试dao层的数据库交互方法,可能会需要去连接数据库。有了Stub和Mock可以降低它们的复杂性,没必要说一定要有数据库连接才能去测试service层的业务逻辑,假如我开发service层的,就没有没必要等同事将DAO层的代码写好,来验证service层的业务逻辑是否正确。
Stub简单介绍
Stub是一个虚拟的物件,一个Stub可以使用最少的依赖方法来模拟该单元测试。简单的说,stub是代码的一部分。在运行时用stub替换真正代码,忽略调用代码的实现。目的是用一个简单一点的行为替换一个复杂的行为,从而独立地测试代码的某一部分。比如,要测试dao层的代码,势必要连接数据库,这时候就可以使用HashMap来模拟数据库操作,这个Stub对象就可以根据你想要的状态去模拟,通过方法去测试Stub的内部状态。可以使用map的方式以及dbUtil来实现stub测试。
利用Stub的Map方法测试dao层代码示例:
/**
* 使用map的方式测试dao
* @author YoYing(yingxy123@163.com)
*
*/
public class TestUserDaoByHashMapImpl {
private UserDao userDao;
private User baseUser;
@Before
public void setUp() {
baseUser = new User(1, "yingxy", "111111", "YoYing");
userDao = new UserDaoByHashMapImpl();
}
@Test
public void testAdd() {
userDao.add(new User(1, "yingxy", "111111", "YoYing"));
User actual = userDao.load("yingxy");
// 判断2个user是否相等,没有重写equals和hashCode方法,直接比较的属性值
AssertHelper.assertUser(baseUser, actual);
}
@Test
public void testDelete() {
userDao.delete("yingxy");
User actual = userDao.load("yingxy");
assertNull(actual);
}
@Test
public void testUpdate() {
String newPassword = "123456";
userDao.add(new User(1, "yingxy", "111111", "YoYing"));
User actual = userDao.load("yingxy");
actual.setPassword(newPassword);
userDao.update(actual);
actual = userDao.load("yingxy");
assertEquals(newPassword, actual.getPassword());
}
}
但是有些时候光测试模拟的数据不能完全测试出sql语句的错误,所以可以使用dbutil这个框架来实现。它的作用是隔离测试数据,基本原理是:测试之前将数据库原有数据备份起来,测试完成之后,清空测试数据,将原来备份出去的数据还原。
DBUtil的基类代码示例:
/**
* dbUtil的辅助类,也是unit test的base类
* @author YoYing(yingxy123@163.com)
*
*/
public abstract class AbstractDBUnit {
public static IDatabaseConnection databaseConn;
private File tempFile;
@BeforeClass
public static void init() {
try {
// 创建一个新的Connection
databaseConn = new DatabaseConnection(DBUtil.getConnection());
} catch (DatabaseUnitException e) {
e.printStackTrace();
}
}
/**
* 创建数据源,读取测试数据,该数据源为xml
* @param tableName
* @return
* @throws DataSetException
*/
protected IDataSet createDateSet(String tableName) throws DataSetException {
// 得到测试数据的xml的输入流,方便保存到DataSet中
InputStream inputStream = AbstractDBUnit.class.getClassLoader().getResourceAsStream(tableName + ".xml");
assertNotNull("dbunit的基本数据不存在", inputStream);
// FlatXmlDataSet是基于属性来存储的
return new FlatXmlDataSet(new FlatXmlProducer(new InputSource(inputStream)));
}
/**
* 备份所有已经存在在数据库表中的数据,将其保存到一个临时文件中,以备以后作恢复用
* @throws SQLException
* @throws IOException
* @throws DataSetException
*/
protected void cleanUpAllTable() throws SQLException, IOException, DataSetException {
// 创建一个与数据库一致的dataSet
IDataSet dataSet = databaseConn.createDataSet();
// 将已有的数据写入临时xml文件中
tempFile = File.createTempFile("back", "xml");
FlatXmlDataSet.write(dataSet, new FileWriter(tempFile));
}
/**
* 备份某几个数据库表的数据
* @param tableName
*/
protected void cleanUpSomeTable(String[] tableName) throws SQLException, IOException, DataSetException {
QueryDataSet queryDataSet = new QueryDataSet(databaseConn);
for(String name : tableName) {
queryDataSet.addTable(name);
}
tempFile = File.createTempFile("back", "xml");
FlatXmlDataSet.write(queryDataSet, new FileWriter(tempFile));
}
/**
* 备份某个数据库表中已有的数据
* @param tableName
*/
protected void cleanUpOneTable(String tableName) throws SQLException, IOException, DataSetException {
this.cleanUpSomeTable(new String[] {tableName});
}
/**
* 还原刚才备份的数据
*/
protected void restoreTable() throws DatabaseUnitException, SQLException, FileNotFoundException {
IDatabaseConnection conn = new DatabaseConnection(DBUtil.getConnection());
IDataSet set = new FlatXmlDataSet(new FlatXmlProducer(
new InputSource(new FileInputStream(tempFile))));
DatabaseOperation.CLEAN_INSERT.execute(conn, set);
}
@AfterClass
public static void destory() {
// 关闭Connection
try {
if (null != databaseConn) {
databaseConn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
使用dbUtil测试dao层代码示例:
public class TestUserServiceImplByDbUtil extends AbstractDBUnit {
private User baseUser;
private IDataSet dataSet;
private UserService userService;
@Before
public void setUp() throws SQLException, IOException, DatabaseUnitException {
baseUser = new User(2, "lutong", "111111", "lt");
// 只清空tb_user这个表的数据
cleanUpOneTable("tb_user");
// tb_user.xml文件,存放预先准备的测试数据
dataSet = createDateSet("tb_user");
userService = new UserServiceImpl(new UserDaoImpl());
}
@After
public void tearDown() throws FileNotFoundException, DatabaseUnitException, SQLException {
// 将原来非测试数据还原
restoreTable();
}
@Test
public void testLoad() throws DatabaseUnitException, SQLException {
DatabaseOperation.CLEAN_INSERT.execute(databaseConn, dataSet);
User user = userService.load("lutong");
AssertHelper.assertUser(user, baseUser);
}
@Test
public void testAddNotExist() throws DatabaseUnitException, SQLException {
// baseUser已经存在数据库表中
DatabaseOperation.CLEAN_INSERT.execute(databaseConn, dataSet);
String username = "yingxy";
User user = new User(null, username, "111111", "YoYing");
userService.add(user);
User actual = userService.load(username);
assertNotNull(actual);
assertEquals(username, actual.getUsername());
}
@Test(expected=TestExecption.class)
public void testAddExist() throws DatabaseUnitException, SQLException {
DatabaseOperation.CLEAN_INSERT.execute(databaseConn, dataSet);
String username = "lutong";
User user = new User(null, username, "111111", "lt");
userService.add(user);
}
@Test
public void testDelete() throws DatabaseUnitException, SQLException {
// baseUser已经存在数据库表中
DatabaseOperation.CLEAN_INSERT.execute(databaseConn, dataSet);
String username = "lutong";
User temp = userService.load(username);
assertNotNull(temp);
// delete
userService.delete(username);
User actual = userService.load(username);
assertNull(actual);
}
@Test
public void testUpdate() throws DatabaseUnitException, SQLException {
// baseUser已经存在数据库表中
DatabaseOperation.CLEAN_INSERT.execute(databaseConn, dataSet);
String username = "lutong";
User temp = userService.load(username);
assertNotNull(temp);
// update
String nickname = "helloKitty";
temp.setNickname(nickname);
userService.update(temp);
User actual = userService.load(username);
assertNotNull(actual);
assertEquals(nickname, actual.getNickname());
}
@Test(expected=TestExecption.class)
public void testLoginUserNotExist() throws DatabaseUnitException, SQLException {
// baseUser已经存在数据库表中
DatabaseOperation.CLEAN_INSERT.execute(databaseConn, dataSet);
String username = "yingxy";
String password = "111111";
userService.login(username, password);
}
@Test(expected=TestExecption.class)
public void testLoginUserErrorPassword() throws DatabaseUnitException, SQLException {
// baseUser已经存在数据库表中
DatabaseOperation.CLEAN_INSERT.execute(databaseConn, dataSet);
String username = "lutong";
String password = "222222";
userService.login(username, password);
}
@Test()
public void testLogin() throws DatabaseUnitException, SQLException {
// baseUser已经存在数据库表中
DatabaseOperation.CLEAN_INSERT.execute(databaseConn, dataSet);
String username = "lutong";
String password = "111111";
User actual = userService.login(username, password);
assertNotNull(actual);
AssertHelper.assertUser(baseUser, actual);
}
}
存放预先准备的测试数据的xml文件,tb_user.xml:
<?xml version="1.0" encoding="UTF-8"?>
<dataset>
<tb_user id="2" username="lutong" password="111111" nickname="lt" />
</dataset>
Mock简单介绍
mocks常适用于把部分代码逻辑的测试同其他的代码隔离开来。mocks不实现任何逻辑,它们只是抽象地提供了一种使测试能控制需要测试的类的所有业务逻辑方法的一个壳子。比如同事A在开发service层,同事B在开发dao层。现在的情况:A的service层的代码已经写完毕了,就等待B开发的dao层完毕并测试好,但是B还没有完成dao层的开发。A这时就可以使用mock对象来模拟dao层的实现,因为A并不需要关注dao层的是否有错(B开发的dao层,也会经过严格的测试,才会提交代码的)。哪些情况下可以使用mock呢?
(1) 真实对象没有确定的行为;
(2) 真实对象难以配置;
(3) 真实对象的一些行为难以控制其发生(比如网络错误);
(4) 真实对象运行缓慢;
(5) 真实对象具有(或者本身就是)用户界面。
在java中的mock的框架有很多,比如JMock,EasyMock,Mockito等等,本文使用的是EasyMock,简单介绍下EasyMock。EasyMock将测试过程分为三步:录制、运行测试代码、验证期望。具体表现为java代码的过程为:
(1) 使用EasyMock生成Mock对象;
(2) 设定Mock对象的预期行为和输出;
(3) 将Mock对象切换到Replay状态;
(4) 调用Mock对象方法进行单元测试;
(5) 对Mock对象的行为进行验证。
更多详细使用EasyMock的教程请看:使用 EasyMock 更轻松地进行测试和EasyMock 使用方法与原理剖析。
使用EasyMock测试service层代码示例:
/**
* 使用mock方式测试service层
* @author YoYing(yingxy123@163.com)
*
*/
public class TestUserServiceImplByMock {
private UserService userService;
private UserDao userDao;
private User standardUser;
@Before
public void setUp() throws Exception {
// 创建了userDao的mock对象
userDao = createStrictMock(UserDao.class);
userService = new UserServiceImpl(userDao);
standardUser = new User(1, "yingxy", "111111", "YoYing");
}
@Test
public void testLoad() {
// 记录userDao发生的操作,由于是createStrictMock
// 所以执行的操作要按照严格的顺序,不然报错
// 当userDao执行load 'yingxy'的时候,会返回standardUser
// userDao.load的被测试方法并不知道测试环境和被测环境的区别,
// 因为他们实现了同一个接口
expect(userDao.load("yingxy")).andReturn(standardUser);
// 测试阶段
replay(userDao);
// 完成测试
User actualUser = userService.load("yingxy");
AssertHelper.assertUser(standardUser, actualUser);
// 验证交互是否正确
verify(userDao);
}
@Test
public void testDelete() {
// 只要测试是否调用
userDao.delete("yingxy");
// 没有返回值使用expectLastCall()方法,有返回时使用expect().andReturn()
expectLastCall();
replay(userDao);
userService.delete("yingxy");
verify(userDao);
}
@Test(expected=TestExecption.class)
public void testAddExist() {
// 模拟"yingxy"用户已经存在
expect(userDao.load(standardUser.getUsername())).andReturn(standardUser);
userDao.add(standardUser);
expectLastCall();
replay(userDao);
userService.add(standardUser);
verify(userDao);
}
@Test
public void testAddNotExist() {
// 返回值为null,模拟添加的yingxy是不存在的
expect(userDao.load("yingxy")).andReturn(null);
userDao.add(standardUser);
expectLastCall();
replay(userDao);
userService.add(standardUser);
verify(userDao);
}
@Test
public void testLoginRight() {
String username = "yingxy";
String password = "111111";
expect(userDao.load(username)).andReturn(standardUser);
replay(userDao);
User actualUser = userService.login(username, password);
AssertHelper.assertUser(standardUser, actualUser);
}
@Test(expected=TestExecption.class)
public void testLoginUsernameIsWrong() {
String username = "guyan";
String password = "111111";
// 该用户不存在,所以返回一个null
expect(userDao.load(username)).andReturn(null);
replay(userDao);
User actualUser = userService.login(username, password);
AssertHelper.assertUser(standardUser, actualUser);
}
@Test(expected=TestExecption.class)
public void testLoginPasswordIsWrong() {
String username = "yingxy";
String password = "222222";
// 该用户是存在的,但是密码错误
expect(userDao.load(username)).andReturn(standardUser);
replay(userDao);
User actualUser = userService.login(username, password);
AssertHelper.assertUser(standardUser, actualUser);
}
}
Mock与Stub的区别
Mock和Stub都是虚拟对象,更准确地说,stub对象可以使用最少的方法来模拟真实的对象。当你的dao依赖数据库的时候,你可以使用HashMap来模拟数据库的操作。Stub对象以及返回的结果大多数是由我们程序员自己创建的。Stub还可以验证方法的内部状态。而Mock对象一般是由框架来帮我们创建的。Mock对象可以验证方法之间的交互是否符合自己期望的。最主要的区别就是:Stub是基于状态的对象,而Mock是基于交互的对象。更为详细的区别请参考:Mocks Aren't Stubs。
由于笔者也是初识Mock和Stub,最主要的还是让大家能够将这2个策略快速使用到自己的单元测试中,故将源码放在GitHub。地址为:https://github.com/YoYing/csdn/tree/master/yingxy-testUtils。
我使用的是Maven搭建的环境,如果朋友们机子上没有Maven的环境,可以添加以下几个jar包:dbunit-2.4.9.jar, easymock-3.1.jar, junit-4.10.jar, mysql-connector-java-5.1.26.jar, slf4j-api-1.6.6.jar, slf4j-simple-1.6.6.jar。由于使用的jdbc,所以先运行mysql.sql文件创建表,如有疑问以及对Stub和Mock有什么不同的见解可以直接发我邮件共同探讨。我的邮箱地址:yingxy123@163.com.