??xml version="1.0" encoding="utf-8" standalone="yes"?> ~写自定义的 Mock 对象需要额外的~码工作Q同时也可能引入错误。EasyMock 提供了根据指定接口动态构?Mock 对象的方法,避免了手工编?Mock 对象。本文将向您展示如何使用 EasyMock q行单元试Qƈ?EasyMock 的原理进行分析?/p>
单元试是对应用中的某一个模块的功能q行验证。在单元试中,我们帔R到的问题是应用中其它的协同模块尚未开发完成,或者被试模块需要和一些不Ҏ构造、比较复杂的对象q行交互。另外,׃不能肯定其它模块的正性,我们也无法确定测试中发现的问题是由哪个模块引L?/p>
Mock 对象能够模拟其它协同模块的行为,被测试模块通过?Mock 对象协作Q可以获得一个孤立的试环境。此外,使用 Mock 对象q可以模拟在应用中不Ҏ构造(?HttpServletRequest 必须?Servlet 容器中才能构造出来)和比较复杂的对象Q如 JDBC 中的 ResultSet 对象Q,从而ɋ试利q行?/p>
手动的构?Mock 对象会给开发h员带来额外的~码量,而且q些为创?Mock 对象而编写的代码很有可能引入错误。目前,有许多开源项目对动态构?Mock 对象提供了支持,q些目能够Ҏ现有的接口或cd态生成,q样不仅能避免额外的~码工作Q同时也降低了引入错误的可能?/p>
EasyMock 是一套用于通过单的Ҏ对于l定的接口生?Mock 对象的类库。它提供Ҏ口的模拟Q能够通过录制、回放、检查三步来完成大体的测试过E,可以验证Ҏ的调用种cR次数、顺序,可以?Mock 对象q回指定的值或抛出指定异常。通过 EasyMockQ我们可以方便的构?Mock 对象从而单元试利q行?/p>
EasyMock 是采?MIT license 的一个开源项目,您可以在 Sourceforge 上下载到相关?zip 文g。目前您可以下蝲?EasyMock 最新版本是2.3Q它需要运行在 Java 5.0 q_上。如果您的应用运行在 Java 1.3 ?1.4 q_上,您可以选择 EasyMock1.2。在解压~?zip 包后Q您可以扑ֈ easymock.jar q个文g。如果您使用 Eclipse 作ؓ IDEQ把 easymock.jar d到项目的 Libraries 里就可以使用了(如下图所C)。此外,׃我们的测试用例运行在 JUnit 环境中,因此您还需?JUnit.jarQ版?.8.1以上Q?/p>
通过 EasyMockQ我们可以ؓ指定的接口动态的创徏 Mock 对象Qƈ利用 Mock 对象来模拟协同模块或是领域对象,从而单元试利q行。这个过E大致可以划分ؓ以下几个步骤Q?/p>
接下来,我们对以上的几个步骤逐一q行说明。除了以上的基本步骤外,EasyMock q对Ҏ?Mock 对象cd、特定的参数匚w方式{功能提供了支持Q我们将在之后的章节中进行说明?/p>
Ҏ指定的接口或c,EasyMock 能够动态的创徏 Mock 对象QEasyMock 默认只支持ؓ接口生成 Mock 对象Q如果需要ؓcȝ?Mock 对象Q在 EasyMock 的主上有扩展包可以实现此功能)Q我们以 通常Q构Z个真实的 我们可以使用 EasyMock 动态构?
EasyMock 是一套通过单的Ҏ对于指定的接口或cȝ?Mock 对象的类库,它能利用Ҏ口或cȝ模拟来辅助单元测试。本文将?EasyMock 的功能和原理q行介绍Qƈ通过CZ来说明如何?EasyMock q行单元试?br />
Mock Ҏ是单元测试中常见的一U技术,它的主要作用是模拟一些在应用中不Ҏ构造或者比较复杂的对象Q从而把试与测试边界以外的对象隔离开?/p>
?QEclipse 目中的 Libraries
ResultSet
接口Z说明EasyMock的功能?code>java.sql.ResultSet 是每一?Java 开发h员都非常熟悉的接口:
清单1QResultSet 接口
public interface java.sql.ResultSet {
......
public abstract java.lang.String getString(int arg0) throws java.sql.SQLException;
public abstract double getDouble(int arg0) throws java.sql.SQLException;
......
}
RecordSet
对象需要经q一个复杂的q程Q在开发过E中Q开发h员通常会编写一?DBUtility
cL获取数据库连?Connection
Qƈ利用 Connection
创徏一?Statement
。执行一?Statement
可以获取C个或多个 ResultSet
对象。这L构造过E复杂ƈ且依赖于数据库的正确q行。数据库或是数据库交互模块出现问题,都会影响单元试的结果?/p>
ResultSet
接口?Mock 对象来解册个问题。一些简单的试用例只需要一?Mock 对象Q这Ӟ我们可以用以下的Ҏ来创?Mock 对象Q?
ResultSet mockResultSet = createMock(ResultSet.class);
其中 createMock
?org.easymock.EasyMock
cL提供的静态方法,你可以通过 static import 其引入Q注Qstatic import ?java 5.0 所提供的新Ҏ)?/p>
如果需要在相对复杂的测试用例中使用多个 Mock 对象QEasyMock 提供了另外一U生成和理 Mock 对象的机Ӟ
IMocksControl control = EasyMock.createControl(); java.sql.Connection mockConnection = control.createMock(Connection.class); java.sql.Statement mockStatement = control.createMock(Statement.class); java.sql.ResultSet mockResultSet = control.createMock(ResultSet.class); |
EasyMock
cȝ createControl
Ҏ能创Z个接?IMocksControl
的对象,该对象能创徏q管理多?Mock 对象。如果需要在试中用多?Mock 对象Q我们推荐您使用q一机制Q因为它在多?Mock 对象的管理上提供了相对便LҎ?/p>
如果您要模拟的是一个具体类而非接口Q那么您需要下载扩展包 EasyMock Class Extension 2.2.2。在对具体类q行模拟Ӟ您只要用 org.easymock.classextension.EasyMock
cM的静态方法代?org.easymock.EasyMock
cM的静态方法即可?/p>
在一个完整的试q程中,一?Mock 对象会l历两个状态:Record 状态和 Replay 状态。Mock 对象一l创建,它的状态就被置?Record。在 Record 状态,用户可以讑֮ Mock 对象的预期行为和输出Q这些对象行录制下来Q保存在 Mock 对象中?/p>
d Mock 对象行ؓ的过E通常可以分ؓ以下3步:
org.easymock.EasyMock
提供的静态方?expectLastCall
获取上一ơ方法调用所对应?IExpectationSetters 实例Q?
IExpectationSetters
实例讑֮ Mock 对象的预期输出?
讑֮预期q回?/strong>
Mock 对象的行为可以简单的理解?Mock 对象Ҏ的调用和Ҏ调用所产生的输出。在 EasyMock 2.3 中,?Mock 对象行ؓ的添加和讄是通过接口 IExpectationSetters
来实现的。Mock 对象Ҏ的调用可能生两U类型的输出Q(1Q生返回|Q?Q抛出异常。接?IExpectationSetters
提供了多U设定预期输出的ҎQ其中和讑֮q回值相对应的是 andReturn ҎQ?
IExpectationSetters<T> andReturn(T value); |
我们仍然?ResultSet
接口?Mock 对象ZQ如果希望方?mockResult.getString(1)
的返回gؓ "My return value"Q那么你可以使用以下的语句:
mockResultSet.getString(1); expectLastCall().andReturn("My return value"); |
以上的语句表C?mockResultSet
?getString
Ҏ被调用一ơ,q次调用的返回值是 "My return value"。有Ӟ我们希望某个Ҏ的调用Lq回一个相同的|Z避免每次调用都ؓ Mock 对象的行行一ơ设定,我们可以用设|默认返回值的ҎQ?
void andStubReturn(Object value); |
假设我们创徏?Statement
?ResultSet
接口?Mock 对象 mockStatement ?mockResultSetQ在试q程中,我们希望 mockStatement 对象?executeQuery
ҎLq回 mockResultSetQ我们可以用如下的语句
mockStatement.executeQuery("SELECT * FROM sales_order_table"); expectLastCall().andStubReturn(mockResultSet); |
EasyMock 在对参数D行匹配时Q默认采?Object.equals()
Ҏ。因此,如果我们?"select * from sales_order_table"
作ؓ参数Q预期方法将不会被调用。如果您希望上例中的 SQL 语句能不区分大小写,可以用特D的参数匚w器来解决q个问题Q我们将?"?EasyMock 中用参数匹配器" 一章对此进行说明?/p>
讑֮预期异常抛出
对象行ؓ的预期输出除了可能是q回值外Q还有可能是抛出异常?code>IExpectationSetters 提供了设定预期抛出异常的ҎQ?
IExpectationSetters<T> andThrow(Throwable throwable); |
和设定默认返回值类|IExpectationSetters
接口也提供了讑֮抛出默认异常的函敎ͼ
void andStubThrow(Throwable throwable); |
讑֮预期Ҏ调用ơ数
通过以上的函敎ͼ您可以对 Mock 对象特定行ؓ的预期输行设定。除了对预期输出q行讑֮Q?code>IExpectationSetters 接口q允许用户对Ҏ的调用次C出限制。在 IExpectationSetters
所提供的这一cL法中Q常用的一U是 times
ҎQ?
IExpectationSetters<T>times(int count); |
该方法可?Mock 对象Ҏ的调用次数进行确切的讑֮。假设我们希?mockResultSet ?getString
Ҏ在测试过E中被调?ơ,期间的返回值都?"My return value"Q我们可以用如下语句Q?
mockResultSet.getString(1); expectLastCall().andReturn("My return value").times(3); |
andReturn
?andThrow
Ҏ的返回g然是一?IExpectationSetters
实例Q因此我们可以在此基上l调?times
Ҏ?/p>
除了讑֮定的调用次敎ͼIExpectationSetters
q提供了另外几种讑֮非准调用次数的ҎQ?br />
times(int minTimes, int maxTimes)
Q该Ҏ最被调用 minTimes ơ,最多被调用 maxTimes ơ?br />
atLeastOnce()
Q该Ҏ臛_被调用一ơ?br />
anyTimes()
Q该Ҏ可以被调用Q意次?
某些Ҏ的返回值类型是 voidQ对于这一cL法,我们无需讑֮q回|只要讄调用ơ数可以了。以 ResultSet
接口?close
ҎZQ假讑֜试q程中,该方法被调用3?ơ:
mockResultSet.close(); expectLastCall().times(3, 5); |
Z化书写,EasyMock q提供了另一U设?Mock 对象行ؓ的语句模式。对于上例,您还可以它写成Q?
expect(mockResult.close()).times(3, 5); |
在生?Mock 对象和设?Mock 对象行ؓ两个阶段QMock 对象的状态都?Record 。在q个阶段QMock 对象会记录用户对预期行ؓ和输出的讑֮?/p>
在?Mock 对象q行实际的测试前Q我们需要将 Mock 对象的状态切换ؓ Replay。在 Replay 状态,Mock 对象能够Ҏ讑֮对特定的Ҏ调用作出预期的响应。将 Mock 对象切换?Replay 状态有两种方式Q您需要根?Mock 对象的生成方式进行选择。如?Mock 对象是通过 org.easymock.EasyMock
cL供的静态方?createMock 生成的(W?节中介绍的第一U?Mock 对象生成ҎQ,那么 EasyMock
cL供了相应?replay Ҏ用于?Mock 对象切换?Replay 状态:
replay(mockResultSet); |
如果 Mock 对象是通过 IMocksControl
接口提供?createMock
Ҏ生成的(W?节中介绍的第二种Mock对象生成ҎQ,那么您依旧可以通过 IMocksControl
接口对它所创徏的所?Mock 对象q行切换Q?
control.replay(); |
以上的语句能在W?节中生成?mockConnection、mockStatement ?mockResultSet {??Mock 对象都切换成 Replay 状态?/p>
Z更好的说?EasyMock 的功能,我们引入 src.zip 中的CZ来解?Mock 对象在实际测试阶D늚作用。其中所有的CZ代码都可以在 src.zip 中找到。如果您使用?IDE ?EclipseQ在导入 src.zip 之后您可以看?Workspace 中增加的 projectQ如下图所C)?/p>
?Q导?src.zip 后的 Workspace
下面是示例代码中的一个接?SalesOrder
Q它的实现类 SalesOrderImpl
的主要功能是从数据库中读取一?Sales Order ?Region ?Total PriceQƈҎd的数据计该 Sales Order ?Price LevelQ完整的实现代码都可以在 src.zip 中找刎ͼQ?/p>
清单2QSalesOrder 接口
public interface SalesOrder { …… public void loadDataFromDB(ResultSet resultSet) throws SQLException; public String getPriceLevel(); } |
其实现类 SalesOrderImpl
中对 loadDataFromDB
的实现如下:
public class SalesOrderImpl implements SalesOrder { ...... public void loadDataFromDB(ResultSet resultSet) throws SQLException { orderNumber = resultSet.getString(1); region = resultSet.getString(2); totalPrice = resultSet.getDouble(3); } ...... } |
Ҏ loadDataFromDB
d?ResultSet
对象包含的数据。当我们之前定义的 Mock 对象调整?Replay 状态,q将该对象作为参C入,那么 Mock 对象的方法将会返回预先定义的预期q回倹{完整的 TestCase 如下Q?/p>
清单4Q完整的TestCase
public class SalesOrderTestCase extends TestCase { public void testSalesOrder() { IMocksControl control = EasyMock.createControl(); ...... ResultSet mockResultSet = control.createMock(ResultSet.class); try { ...... mockResultSet.next(); expectLastCall().andReturn(true).times(3); expectLastCall().andReturn(false).times(1); mockResultSet.getString(1); expectLastCall().andReturn("DEMO_ORDER_001").times(1); expectLastCall().andReturn("DEMO_ORDER_002").times(1); expectLastCall().andReturn("DEMO_ORDER_003").times(1); mockResultSet.getString(2); expectLastCall().andReturn("Asia Pacific").times(1); expectLastCall().andReturn("Europe").times(1); expectLastCall().andReturn("America").times(1); mockResultSet.getDouble(3); expectLastCall().andReturn(350.0).times(1); expectLastCall().andReturn(1350.0).times(1); expectLastCall().andReturn(5350.0).times(1); control.replay(); ...... int i = 0; String[] priceLevels = { "Level_A", "Level_C", "Level_E" }; while (mockResultSet.next()) { SalesOrder order = new SalesOrderImpl(); order.loadDataFromDB(mockResultSet); assertEquals(order.getPriceLevel(), priceLevels[i]); i++; } control.verify(); } catch (Exception e) { e.printStackTrace(); } } } |
在这个示例中Q我们首先创Z ResultSet
?Mock 对象 moResultSetQƈ记录?Mock 对象的预期行为。之后我们调用了 control.replay()
Q将 Mock 对象的状态置?Replay 状态。在实际的测试阶D,Sales Order 对象?loadDataFromDB
Ҏ调用?mockResultSet 对象?getString
?getDouble
Ҏd mockResultSet 中的数据。Sales Order 对象Ҏd的数据计出 Price LevelQƈ和预期输行比较?
在利?Mock 对象q行实际的测试过E之后,我们q有一件事情没有做Q对 Mock 对象的方法调用的ơ数q行验证?/p>
Z验证指定的方法调用真的完成了Q我们需要调?verify
Ҏq行验证。和 replay
ҎcMQ您需要根?Mock 对象的生成方式来选用不同的验证方式。如?Mock 对象是由 org.easymock.EasyMock
cL供的 createMock
静态方法生成的Q那么我们同样采?EasyMock
cȝ静态方?verify
q行验证Q?
verify(mockResultSet); |
如果Mock对象是有 IMocksControl
接口所提供?createMock
Ҏ生成的,那么采用该接口提供的 verify
ҎQ例如第1节中?IMocksControl
实例 controlQ?
control.verify(); |
对 control 实例所生成?Mock 对象 mockConnection、mockStatement ?mockResultSet {进行验证。如果将上例?expectLastCall().andReturn(false).times(1)
的预期次C改ؓ2Q在 Eclipse 中将可以看到Q?/p>
?QMock对象验证p|
Z避免生成q多?Mock 对象QEasyMock 允许对原?Mock 对象q行重用。要?Mock 对象重新初始化,我们可以采用 reset Ҏ。和 replay ?verify ҎcMQEasyMock 提供了两U?reset 方式Q(1Q如?Mock 对象是由 org.easymock.EasyMock
cM的静态方?createMock
生成的,那么?Mock 对象的可以用 EasyMock
cȝ静态方?reset
重新初始化;Q?Q如?Mock Ҏ是由 IMocksControl
实例?createMock
Ҏ生成的,那么?IMocksControl
实例Ҏ reset
的调用将会把所有该实例创徏?Mock 对象重新初始化?/p>
在重新初始化之后QMock 对象的状态将被置?Record 状态?/p>
在?Mock 对象q行实际的测试过E中QEasyMock 会根据方法名和参数来匚w一个预期方法的调用。EasyMock 对参数的匚w默认使用 equals()
Ҏq行比较。这可能会引起一些问题。例如在上一章节中创建的mockStatement对象Q?
mockStatement.executeQuery("SELECT * FROM sales_order_table"); expectLastCall().andStubReturn(mockResultSet); |
在实际的调用中,我们可能会遇?SQL 语句中某些关键字大小写的问题Q例如将 SELECT 写成 SelectQ这时在实际的测试中QEasyMock 所采用的默认匹配器认两个参数不匹配,从而造成 Mock 对象的预期方法不被调用。EasyMock 提供了灵zȝ参数匚w方式来解册个问题。如果您?mockStatement 具体执行的语句ƈ不关注,q希望所有输入的字符串都能匹配这一Ҏ调用Q您可以?org.easymock.EasyMock
cL提供?anyObject
Ҏ来代替参C?SQL 语句Q?
mockStatement.executeQuery( anyObject() ); expectLastCall().andStubReturn(mockResultSet); |
anyObject
Ҏ表示L输入值都与预期值相匚w。除?anyObject
以外QEasyMockq提供了多个预先定义的参数匹配器Q其中比较常用的一些有Q?/p>
aryEq(X value)
Q通过Arrays.equals()
q行匚wQ适用于数l对象;
isNull()
Q当输入gؓNull时匹配;
notNull()
Q当输入g为Null时匹配;
same(X value)
Q当输入值和预期值是同一个对象时匚wQ?
lt(X value), leq(X value), geq(X value), gt(X value)
Q当输入值小于、小{于、大{于、大于预期值时匚wQ适用于数值类型;
startsWith(String prefix), contains(String substring), endsWith(String suffix)
Q当输入g预期值开头、包含预期倹{以预期值结时匚wQ适用于StringcdQ?
matches(String regex)
Q当输入g正则表达式匹配时匚wQ适用于Stringcd?预定义的参数匚w器可能无法满一些复杂的情况Q这时你需要定义自q参数匚w器。在上一节中Q我们希望能有一个匹配器?SQL 中关键字的大写不敏感,使用 anyObject
其实q不是一个好的选择。对此,我们可以定义自己的参数匹配器 SQLEquals?/p>
要定义新的参数匹配器Q需要实?org.easymock.IArgumentMatcher
接口。其中,matches(Object actual)
Ҏ应当实现输入值和预期值的匚w逻辑Q而在 appendTo(StringBuffer buffer)
Ҏ中,你可以添加当匚wp|旉要显C的信息。以下是 SQLEquals 实现的部分代码(完整的代码可以在 src.zip 中找刎ͼQ?/p>
清单5Q自定义参数匚w器SQLEquals
public class SQLEquals implements IArgumentMatcher { private String expectedSQL = null; public SQLEquals(String expectedSQL) { this.expectedSQL = expectedSQL; } ...... public boolean matches(Object actualSQL) { if (actualSQL == null && expectedSQL == null) return true; else if (actualSQL instanceof String) return expectedSQL.equalsIgnoreCase((String) actualSQL); else return false; } } |
在实C IArgumentMatcher
接口之后Q我们需要写一个静态方法将它包装一下。这个静态方法的实现需要将 SQLEquals 的一个对象通过 reportMatcher
Ҏ报告lEasyMockQ?/p>
清单6Q自定义参数匚w?SQLEquals 静态方?/strong>
public static String sqlEquals(String in) { reportMatcher(new SQLEquals(in)); return in; } |
q样Q我们自定义?sqlEquals 匚w器就可以使用了。我们可以将上例中的 executeQuery
Ҏ讑֮修改如下Q?
mockStatement.executeQuery(sqlEquals("SELECT * FROM sales_order_table")); expectLastCall().andStubReturn(mockResultSet); |
executeQuery("select * from sales_order_table")
q行Ҏ调用Ӟ该预期行为将被匹配?
![]() ![]() |
到目前ؓ止,我们所创徏?Mock 对象都属?EasyMock 默认?Mock 对象cdQ它寚w期方法的调用ơ序不敏感,寚w预期的方法调用抛?AssertionError。除了这U默认的 Mock cd以外QEasyMock q提供了一些特D的 Mock cd用于支持不同的需求?/p>
如果 Mock 对象是通过 EasyMock.createMock()
或是 IMocksControl.createMock()
所创徏的,那么在进?verify 验证ӞҎ的调用顺序是不进行检查的。如果要创徏Ҏ调用的先后次序敏感的 Mock 对象QStrick MockQ,应该使用 EasyMock.createStrickMock()
来创建,例如Q?
ResultSet strickMockResultSet = createStrickMock(ResultSet.class); |
cM?createMockQ我们同样可以用 IMocksControl
实例来创Z?Strick Mock 对象Q?
IMocksControl control = EasyMock.createStrictControl(); ResultSet strickMockResultSet = control.createMock(ResultSet.class); |
使用 createMock()
创徏?Mock 对象寚w预期的方法调用默认的行ؓ是抛?AssertionErrorQ如果需要一个默认返?Qnull ?false {?无效??"Nice Mock" 对象Q可以通过 EasyMock
cL供的 createNiceMock()
Ҏ创徏。类似的Q你也可以用 IMocksControl
实例来创Z?Nice Mock 对象?/p>
![]() ![]() |
EasyMock 是如何ؓ一个特定的接口动态创?Mock 对象Qƈ记录 Mock 对象预期行ؓ的呢Q其实,EasyMock 后台处理的主要原理是利用 java.lang.reflect.Proxy
为指定的接口创徏一个动态代理,q个动态代理,是我们在编码中用到?Mock 对象。EasyMock qؓq个动态代理提供了一?InvocationHandler
接口的实玎ͼq个实现cȝ主要功能是动态代理的预期行ؓ记录在某个映表中和在实际调用时从这个映表中取出预期输出。下图是 EasyMock 中主要的功能c:
和开发h员联pL紧密的是 EasyMock
c,q个cL供了 createMock、replay、verify
{方法以及所有预定义的参数匹配器?/p>
我们知道 Mock 对象有两U创建方式:一U是通过 EasyMock
cL供的 createMock
Ҏ创徏Q另一U是通过 EasyMock
cȝ createControl
Ҏ得到一?IMocksControl
实例Q再p?IMocksControl
实例创徏 Mock 对象。其实,无论通过哪种Ҏ获得 Mock 对象QEasyMock 都会生成一?IMocksControl
的实例,只不q第一U方式中?IMocksControl
的实例对开发h员不可见而已。这?IMocksControl
的实例,其实是 MocksControl
cȝ一个对象?code>MocksControl cL供了 andReturn、andThrow、times、createMock
{方法?/p>
MocksControl
cM包含了两个重要的成员变量Q分别是接口 IMocksBehavior
?IMocksControlState
的实例。其中,IMocksBehavior
的实现类 MocksBehavior
?EasyMock 的核心类Q它保存着一?ExpectedInvocationAndResult
对象的一个列表,?ExpectedInvocationAndResult
对象中包含着 Mock 对象Ҏ调用和预期结果的映射?code>MocksBehavior cL供了 addExpected
?addActual
Ҏ用于d预期行ؓ和实际调用?/p>
MocksControl
cM包含的另一个成员变量是 IMocksControlState
实例?code>IMocksControlState 拥有两个不同的实现类Q?code>RecordState ?ReplayState
。顾名思义Q?code>RecordState ?Mock 对象?Record 状态时的支持类Q它提供?invoke
Ҏ?Record 状态下的实现。此外,它还提供?andReturn、andThrow、times
{方法的实现?code>ReplayState ?Mock 对象?Replay 状态下的支持类Q它提供?invoke
Ҏ?Replay 状态下的实现。在 ReplayState 中,andReturn、andThrow、times
{方法的实现都是抛出IllegalStateExceptionQ因为在 Replay 阶段Q开发h员不应该再调用这些方法?/p>
当我们调?MocksControl
?createMock
ҎӞ该方法首先会生成一?JavaProxyFactory
cȝ对象?code>JavaProxyFactory 是接?IProxyFactory
的实现类Q它的主要功能就是通过 java.lang.reflect.Proxy
Ҏ定的接口创徏动态代理实例,也就是开发h员在外部看到?Mock 对象?/p>
在创建动态代理的同时Q应当提?InvocationHandler
的实现类?code>MockInvocationHandler 实现了这个接口,它的 invoke
Ҏ主要的功能是Ҏ Mock 对象状态的不同而分别调?RecordState
?invoke
实现或是 ReplayState
?invoke
实现?/p>
下图是创?Mock 对象的时序图Q?/p>
?Q创?Mock 对象时序?/strong>
?EasyMock
cȝ createMock
Ҏ被调用时Q它首先创徏一?MocksControl
对象Qƈ调用该对象的 createMock
Ҏ创徏一?JavaProxyFactory
对象和一?MockInvocationHandler
对象?code>JavaProxyFactory 对象?MockInvocationHandler
对象作ؓ参数Q通过 java.lang.reflect.Proxy
cȝ newProxyInstance
静态方法创Z个动态代理?/p>
记录 Mock 的预期行为可以分Z个阶D:预期Ҏ的调用和预期输出的设定。在外部E序中获得的 Mock 对象Q其实就是由 JavaProxyFactory
创徏的指定接口的动态代理,所有外部程序对接口Ҏ的调用,都会指向 InvocationHandler
实现cȝ invoke
Ҏ。在 EasyMock 中,q个实现cL MockInvocationHandler
。下图是调用预期Ҏ的时序图Q?/p>
?Q调用预期方法时序图
?MockInvocationHandler
?invoke
Ҏ被调用时Q它首先通过 reportLastControl
静态方法将 Mock 对象对应?MocksControl
对象报告l?LastControl
c,LastControl
cd该对象保存在一?ThreadLocal 变量中。接着Q?code>MockInvocationHandler 创Z?Invocation 对象Q这个对象将保存预期调用?Mock 对象、方法和预期参数?/p>
在记?Mock 对象预期行ؓӞMock 对象的状态是 Record 状态,因此 RecordState
对象?invoke
Ҏ被调用。这个方法首先调?LastControl
?pullMatchers
Ҏ获取参数匚w器。如果您q记得自定义参数匚w器的q程Q应该能惌v参数匚w器被调用时会实现类的实例报告给 EasyMockQ而这个实例最l保存在 LastControl
中。如果没有指定参数匹配器Q默认的匚w器将会返回给 RecordState
?/p>
Ҏ Invocation
对象和参数匹配器Q?code>RecordState 创Z?ExpectedInvocation
对象q保存下来?/p>
在对预期Ҏq行调用之后Q我们可以对该方法的预期输出q行讑֮。我们以
expectLastCall().andReturn(X value).times(int times) |
times
Ҏ未被昑ּ的调用,EasyMock 会默认作?times(1)
处理。下图是讑֮预期输出的时序图Q?
在预期方法被调用ӞMock 对象对应?MocksControl
对象引用已经记录?LastControl
中,expectLastCall
Ҏ通过调用 LastControl
?lastControl
Ҏ可以获得q个引用?code>MocksControl 对象?andReturn
Ҏ?Mock 对象 Record 状态下会调?RecordState
?andReturn
ҎQ将讑֮的预期输Z Result
对象的Ş式记录下来,保存?RecordState
?lastResult 变量中?/p>
?MocksControl
?times
Ҏ被调用时Q它会检?RecordState
?lastResult 变量是否为空。如果不为空Q则?lastResult 和预期方法被调用时创建的 ExpectedInvocation
对象一P作ؓ参数传递给 MocksBehavior
?addExpected
Ҏ?code>MocksBehavior ?addExpected
Ҏ这些信息保存在数据列表中?/p>
EasyMock
cȝ replay
Ҏ可以?Mock 对象切换?Replay 状态。在 Replay 状态下QMock 对象根据之前的讑֮q回预期输出。下图是 Replay 状态下 Mock 对象Ҏ调用的时序图Q?/p>
?Q调?Mock 对象Ҏ时序?/strong>
?Replay 状态下Q?code>MockInvocationHandler 会调?ReplayState
?invoke
Ҏ。该Ҏ会把 Mock 对象通过 MocksBehavior
?addActual
Ҏd到实际调用列表中Q该列表?verify
Ҏ被调用时被用到。同ӞaddActual
Ҏ会根据实际方法调用与预期Ҏ调用q行匚wQ返回对应的 Result
对象。调?Result
对象?answer
Ҏ可以获取该Ҏ调用的输出?/p>
![]() ![]() |
如果您需要在单元试中构?Mock 对象来模拟协同模块或一些复杂对象,EasyMock 是一个可以选用的优U框架。EasyMock 提供了简便的Ҏ创徏 Mock 对象Q通过定义 Mock 对象的预期行为和输出Q你可以讑֮?Mock 对象在实际测试中被调用方法的q回倹{异常抛出和被调用次数。通过创徏一个可以替代现有对象的 Mock 对象QEasyMock 使得开发h员在试时无需~写自定义的 Mock 对象Q从而避免了额外的编码工作和因此引入错误的机会?/p>