Mock
对象能够模拟领域对象的部分行为,并且能够检验运行结果是否和预期的一致。领域类将通过与
Mock
对象的交互,来获得一个独立的测试环境(引自《
精通
Spring——Java
轻量级架构开发实践
》。
在模仿对象中,我们定义了四个概念:
1
)目标对象:正在测试的对象
2
)合作者对象:由目标对象创建或获取的对象
3
)模仿对象:遵循模仿对象模式的合作者的子类(或实现)
4
)特殊化对象:覆盖创建方法以返回模仿对象而不是合作者的目标的子类
一般来说,应用模仿对象的过程如下:
1
)创建模仿对象的实例
2
)设置模仿对象中的状态和期望值
3
)将模仿对象作为参数来调用域代码
4
)验证模仿对象中的一致性
那么,我们应该如何以及在哪里使用
Mock
对象呢?一般来说,对于目标对象中的合作者对象,在测试时如果其状态或行为的实现严重地依赖外部资源(比如数据持久化中的
DAO
,比如负责发送电子邮件的类),或者团队并行开发时,目标对象的合作者对象并没有实现(比如
J2EE
中,横向分工时,负责
Action
的调用
Service
,负责
Service
调用
DAO
时,相应的
Service
及
DAO
没有实现),这时我们就需要模仿这些类。其实,在做
J2EE
时,传统的
N
层架构中,我们都是面向接口编程的,我们定义了
DAO
接口,我们定义了
Service
接口,这样做的优点就是我们在测试时可以构造实现接口的
Mock
类。这里不得不提依赖注入,通过依赖注入,我们才能在测试时
set Mock
对象。这也说明,为了方便测试,我们不得不一步一步
重构代码,而模式就在重构中自然地产生了。
对于
Mock
对象,我们可以根据
合作者接口(或者是类)
实现具体的
Mock
类,这样的
Mock
类实际上是
Stub
。有些情况下,
Stub
是必要的。但对于诸如
DAO
、
Service
,我们只关心在给定参数的情况下,调用的方法能够返回预期的值,我们根本不关心其内部实现,这时候如果使用
Stub
的话就会产生不必要的代码。我们需要的就是能
动态地生成
Mock
对象而不需要编写它们的工具,
EasyMock
就是这样的工具。
EasyMock
是一个
Mock
对象的类库,现在的版本是
2.0
,这个版本只支持
Mock
接口,如果需要
Mock
类,需要下载它的扩展包。
下面通过一个具体的例子说明一下
Mock
对象的使用,我写的例子就是测试
Service
类中的一个方法,
Mock
的对象是
DAO
。
首先是一个简单的实体
bean Account:
package
easymocktest.domain;
import
org.apache.commons.lang.builder.ToStringBuilder;
import
org.apache.commons.lang.builder.ToStringStyle;
public
class
Account
{
private
Long id;
private
String name;
private
String pwd;
public
Long getId()
{
return
id;
}
public
void
setId(Long id)
{
this
.id
=
id;
}
public
String getName()
{
return
name;
}
public
void
setName(String name)
{
this
.name
=
name;
}
public
String getPwd()
{
return
pwd;
}
public
void
setPwd(String pwd)
{
this
.pwd
=
pwd;
}
/** */
/**
*
@see
java.lang.Object#toString()
*/
public
String toString()
{
return
new
ToStringBuilder(
this
, ToStringStyle.MULTI_LINE_STYLE)
.append(
"
name
"
,
this
.name).append(
"
pwd
"
,
this
.pwd).append(
"
id
"
,
this
.id).toString();
}
}
一个只含一个方法的
DAO AccountDAO:
package
easymocktest.dao;
import
easymocktest.domain.
*
;
public
interface
AccountDAO
{
public
Account getByNameAndPwd(String name,String pwd);
}
一个同样只含一个方法的
AccountService
接口:
package
easymocktest.service;
import
easymocktest.domain.
*
;
public
interface
AccountService
{
public
Account getAccount(String name,String pwd);
}
与
AccountService
相应的实现类:
package
easymocktest.service.impl;
import
easymocktest.service.AccountService;
import
easymocktest.dao.
*
;
import
easymocktest.domain.Account;
public
class
AccountServiceImpl
implements
AccountService
{
private
AccountDAO accountDAO;
public
AccountDAO getAccountDAO()
{
return
accountDAO;
}
public
void
setAccountDAO(AccountDAO accountDAO)
{
this
.accountDAO
=
accountDAO;
}
public
Account getAccount(String name, String pwd)
{
return
this
.accountDAO.getByNameAndPwd(name, pwd);
}
}
这里我没有实现
AccountDAO
接口,对于
Mock
测试来说,这也是不需要的。下面就是
AccountServiceImpl
的测试类
AccountServiceTest
:
package
easymocktest.service;
import
junit.framework.
*
;
import
easymocktest.dao.
*
;
import
easymocktest.domain.
*
;
import
easymocktest.service.impl.
*
;
import
static
org.easymock.EasyMock.createMock;
import
static
org.easymock.EasyMock.replay;
import
static
org.easymock.EasyMock.reset;
import
static
org.easymock.EasyMock.verify;
import
static
org.easymock.EasyMock.expect;
public
class
AccountServiceTest
extends
TestCase
{
private
AccountDAO accountDAOMock;
private
AccountServiceImpl accountService;
@Override
protected
void
setUp()
throws
Exception
{
accountDAOMock
=
createMock(AccountDAO.
class
);
accountService
=
new
AccountServiceImpl();
accountService.setAccountDAO(accountDAOMock);
}
public
void
testGetAccount()
{
String name
=
"
kafka
"
;
String pwd
=
"
0102
"
;
Account a
=
new
Account();
a.setName(name);
a.setPwd(pwd);
a.setId(
new
Long(
10
));
reset(accountDAOMock);
//
(a)
expect(accountDAOMock.getByNameAndPwd(name, pwd)).andReturn(a);
//
(b)
replay(accountDAOMock);
//
(c)
Account b
=
accountService.getAccount(name, pwd);
assertEquals(a, b);
verify(accountDAOMock);
//
(d)
}
}
下面简要的说明一下
Mock
对象的工作过程:
1
)在
setUp()
中,通过
“accountDAOMock = createMock(AccountDAO.class);”
(这里使用了
java5
中的静态导入),创建
AccountDAO
的
Mock
对象,由于
EasyMock
采用了范型技术,故创建的
Mock
对象不需要强制类型转换。然后通过
“accountService.setAccountDAO(accountDAOMock);”
设置目标对象的合作者对象。
2
)对于测试方法
“testGetAccount()”
,
(a)
处的
reset()
方法是将
Mock
对象复位,也就是重新设置
Mock
对象的状态和行为。由于此处是第一次调用
Mock
对象,可以不必使用
reset()
方法。
3
)
(b)
处
expect()
是录制
Mock
对象方法的调用,其参数就是
Mock
对象的方法,其中如果调用的方法有返回值,要通过
andReturn()
方法设置预期的返回值。
4
)
(c)
处的
replay()
是结束录制过程。
在调用
replay()
方法之前的状态,
EashMock
称之为
“record
状态
”
。该状态下,
Mock
对象不具备行为(即模拟接口的实现),它仅仅记录方法的调用。在调用
replay()
后,它才以
Mock
对象预期的行为进行工作,检查预期的方法调用是否真的完成。
5
)
(d)
处的
verify()
是用于在录制和回放两个步骤完成之后进行预期和实际结果的检查。这里就是检查
accountDAOMock
是否如预期一样调用了
getByNameAndPwd
方法。
对于上面的举例,它可能并不具有实际的价值,这里我只想抛砖引玉。在
N
层架构的
Java
程序中,
Mock
对象在单元测试中正发挥着越来越重要的作用。我现在看到的是,在
Service
层与
Web
层,
Mock
对象能很好的被应用。有人觉得在
Persistence
层也应该使用
Mock
对象,但就像我们所知道的,在使用
Hibernate
、
Ibatis
等
ORM
工具的情况下,我们的
Persistence
层的测试主要测试的就是那些配置文件、查询语句等(实际上是集成测试),如果还
Mock
的话,就失去了测试的意义。
对于
Mock
的更多的信息,你可以访问Mock Objects,在本文写作的过程中,参考了使用模仿对象进行单元测试
等文章,一并感谢。