EasyMock是一套通过简单的方法对于指定的接口或类生成Mock对象的类库,它能利用对接口或类的模拟来辅助单元测试。
要使用EasyMock辅助单元测试,添加easymock的jar包即可。
通过EasyMock,我们可以为指定的接口动态地创建Mock对象,并利用Mock对象来模拟协同模块或领域对象,从而使单元测试顺利进行。此过程可划分为以下步骤:
根据指定的接口或类,EasyMock能动态地创建Mock对象。
Eg:以ResultSet
接口为例
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
对象需要经过一个复杂的过程:在开发过程中,开发人员通常会编写一个 DBUtility
类来获取数据库连接 Connection
,并利用 Connection
创建一个 Statement
。执行一个 Statement
可以获取到一个或多个 ResultSet
对象。这样的构造过程复杂并且依赖于数据库的正确运行。数据库或是数据库交互模块出现问题,都会影响单元测试的结果。
我们可以使用 EasyMock 动态构建 ResultSet
接口的 Mock 对象来解决这个问题。
static import org.easymock.Easymock; //静态方法引入 ResultSet mockResultSet = createMock(ResultSet.class);
或
import org.easymock.Easymock; ResultSet mockResultSet = Easymock.createMock(ResultSet.class);
EasyMock默认只支持为接口生成Mock对象,若要为类生成Mock对象,需下载扩展包 EasyMock Class Extension
,在对具体类进行模拟时,只需把 org.easymock.EasyMock
替换为 org.easymock.classextension.Easymock
。
若在相对复杂的测试用例中使用多个Mock对象,可使用EasyMock提供的生成和管理Mock对象的机制。
EasyMock类的createControl
方法能创建一个接口IMocksControl
的对象,此对象能创建并管理多个Mock对象。
Eg:
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);
在一个完整测试中,一个Mock对象会经历两个状态:Record状态和Replay状态。
Mock对象一经创建,状态为Record,Record状态下用户可以设定Mock对象的预期行为和输出,这些对象行为会被录制下来,保存在Mock对象中。
添加Mock对象行为的过程分为三步:
(Mock对象的行为可简单理解为Mock对象的调用和方法调用所产生的输出。)
设定返回值对应接口IExpectionSetters
的andReturn
方法。
IExpectationSetters<T> andReturn(T value);
Eg:仍用 ResultSet
接口的 Mock 对象为例,若希望方法 mockResultSet.getString(1)
的返回值为 "My return value",则:
mockResultSet.getString(1); EasyMock.expectLastCall().andReturn("My return value");
或:
EasyMock.expect(mockResultSet.getString(1)).andReturn("My return value");
若希望 某方法的调用总是返回一个相同的值 ,为避免每次调用都为Mock对象的行为进行一次设定,可用默认返回值的方法——andSubReturn
方法。
void andStubReturn(Object value);
Eg:假设我们创建了 Statement
和 ResultSet
接口的 Mock 对象 mockStatement 和 mockResultSet,在测试过程中,我们希望 mockStatement 对象的 executeQuery
方法总是返回 mockResultSet,则:
mockStatement.executeQuery("SELECT * FROM sales_order_table"); EasyMock.expectLastCall().andStubReturn(mockResultSet);
或:
EasyMock.expect(mockStatement.executeQuery("SELECT * FROM sales_order_table")).andStubReturn(mockResultSet);
若方法的返回值类型为void,则对于此类方法,我们无需设定返回值,只需设置调用次数就可以。(也可以不设置)
How设定调用次数,详看2.2.3。
Eg:以 ResultSet
接口的 close
方法为例,假设在测试过程中,该方法被调用3至5次,则:
mockResultSet.close(); EasyMock.expectLastCall().times(3,5);// 最新版本的EasyMock可以忽略此句
或:
EasyMock.expect(mockResult.close()).times(3, 5);
设定预期抛出异常对应接口IExpectionSetters
的andThrow
方法。
IExpectationSetters<T> andThrow(Throwable throwable);
类似的,设定抛出默认异常对应接口IExpectionSetters
的andStubThrow
方法。
void andStubThrow(Throwable throwable);
Eg:
EasyMock.expectLastCall().andThrow( new MyException(new RuntimeException())).anyTimes();
IExpectionSetters
的times
方法。IExpectationSetters<T>times(int count);
Eg:我们希望 mockResultSet 的 getString
方法在测试过程中被调用3次,期间的返回值都是 "My return value",则:
mockResultSet.getString(1); expectLastCall().andReturn("My return value").times(3);
或:
EasyMock.expect(mockResultSet.getString(1)).andReturn("My return value").times(3);
times(int minTimes, int maxTimes)
:该方法最少被调用 minTimes 次,最多被调用 maxTimes 次。atLeastOnce()
:该方法至少被调用一次。anyTimes()
:该方法可以被调用任意次。很可能某个方法期望的返回结果不是固定的,例如根据传入参数不同而不同;这时需要使用andAnswer()方法:
EasyMock.expect(mockService.execute(EasyMock.anyInt())).andAnswer(new IAnswer<Integer>() { public Integer answer() throws Throwable { Integer count = (Integer) EasyMock.getCurrentArguments()[0]; return count * 2; } });
{% note warning %}
注意:通过EasyMock.getCurrentArguments()
可以获取传入参数!
{% endnote %}
在生成Mock对象和设定Mock对象行为的两个阶段,Mock对象的状态均为Record,此阶段Mock对象会记录用户对预期行为和输出的设定。
在使用Mock对象隐形实际的测试前,需将Mock对象的状态切换为Replay,此阶段Mock对象能根据设定对特定的方法调用作出预期的响应。
将对象切换到Replay状态有两种方法:
若Mock对象是通过createMock方法生成
EasyMock.replay(mockResultSet);
若Mock对象通过createControl方法生成的接口对象control的createMock方法生成
control.replay();
此语句可将通过coltrol的createMock方法生成的所有Mock对象均切换为Replay状态。
此部分放到JUnit+EasyMock实例中。
在利用Mock对象进行实际的测试过程后,还需对Mock对象的方法调用的次数进行验证。
若Mock对象是通过createMock方法生成
EasyMock.verify(mockResultSet);
若Mock对象通过createControl方法生成的接口对象control的createMock方法生成
control.verify();
同理,此语句可验证通过coltrol的createMock方法生成的所有Mock对象方法的调用次数。
为避免生成过多的Mock对象,EasyMock允许对原有的Mock对象进行重用。
可使用reset方法对Mock对象重新初始化。重新初始化后,Mock对象被置为Record状态。
若Mock对象是通过createMock方法生成
EasyMock.reset(mockResultSet);
若Mock对象通过createControl方法生成的接口对象control的createMock方法生成
control.reset();
同理,此语句可验证通过coltrol的createMock方法生成的所有Mock对象重新初始化。
使用Mock对象进行实际的测试过程中,EasyMock会根据方法名和参数来匹配一个预期方法的调用。
此时,EasyMock对参数的匹配默认使用equals()方法进行比较,这可能会引起一些问题,因此EasyMock提供了一些参数匹配方式。
{% note info %}
如2.2.1.1中创建的mockStatement对象:
mockStatement.executeQuery("SELECT * FROM sales_order_table"); EasyMock.expectLastCall().andStubReturn(mockResultSet);
实际调用中,可能会遇到SQL语句中某些关键词大小写问题(因为SQL语句不区分大小写)如将SELECT写成select,此时EasyMock采用的默认匹配器equals方法将认为参数不匹配,则Mock对象的预期方法不会被调用。
{% endnote %}
anyObject()
:任意输入值都与预期值匹配;aryEq(X value)
:通过Arrays.equals()
进行匹配,适用于数组对象;isNull()
:当输入值为Null时匹配;notNull()
:当输入值不为Null时匹配;same(X value)
:当输入值和预期值是同一个对象时匹配;lt(X value), leq(X value), geq(X value), gt(X value)
:当输入值小于、小等于、大等于、大于预期值时匹配,适用于数值类型;startsWith(String prefix), contains(String substring), endsWith(String suffix)
:当输入值以预期值开头、包含预期值、以预期值结尾时匹配,适用于String类型;matches(String regex)
:当输入值与正则表达式匹配时匹配,适用于String类型。例如:若我对mockStatement具体执行的语句并不关注,希望所有输入的字符串都能够匹配这一方法的调用
EasyMock.expect(mockStatement.executeQuery(anyObject())).andStubReturn(mockResultSet);
详细参阅 easymock教程-参数匹配
预定义的参数匹配器无法满足一些复杂情况,此时需自己定义参数匹配器。
如:在3.1中我们希望有一个匹配器对SQL中关键词的大小写不敏感,使用anyObject其实并不好,此时我们可以自定义参数匹配器SQLEquals。
How自定义参数匹配器:
实现 org.easymock.IArgumentMatcher
接口
matches(Object actual)
方法应当实现输入值和预期值的匹配逻辑
appendTo(StringBuffer buffer)
方法可以添加当匹配失败时需要显示的信息
使用静态方法包装实现接口的类
package org.easymock.demo.matcher; import static org.easymock.EasyMock.reportMatcher; import org.easymock.IArgumentMatcher; //实现IArgumentMatcher接口 public class SQLEquals implements IArgumentMatcher { private String expectedSQL = null; public SQLEquals(String expectedSQL) { this.expectedSQL = expectedSQL; } //当匹配失败时需要显示的信息 public void appendTo(StringBuffer buffer) { buffer.append("SQLEquals(\"" + 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; } //自定义参数匹配器SQLEquals静态方法 public static String sqlEquals(String in) { reportMatcher(new SQLEquals(in)); return in; } }
使用自定义的sqlEquals匹配器:
EasyMock.expect(mockStatement.executeQuery(sqlEquals("SELECT * FROM sales_order_table"))).andStubReturn(mockResultSet);
上述创建的Mock对象都属于EasyMock默认的Mock对象类型,它对预期方法的调用顺序不敏感,对非预期的方法调用抛出AssertionError。
除此默认类型,EasyMock还提供一些特殊的Mock类型用于支持不同的需求。
Stick Mock对象——对方法调用的先后顺序敏感,创建方法如下:
使用 EasyMock.createStrickMock()
来创建:
ResultSet strickMockResultSet = EasyMock.createStrickMock(ResultSet.class);
类似于createMock,同样可用IMocksControl
实例来创建一个Stick Mock对象:
IMocksControl control = EasyMock.createStrictControl(); ResultSet strickMockResultSet = control.createMock(ResultSet.class);
Nice Mock对象——默认返回0,null或false等“无效值”,创建方法如下:
使用 EasyMock.createNiceMock()
来创建:
ResultSet strickMockResultSet = EasyMock.createNiceMock(ResultSet.class);
类似于createMock,同样可用IMocksControl
实例来创建一个Nice Mock对象:
IMocksControl control = EasyMock.createNiceControl(); ResultSet strickMockResultSet = control.createMock(ResultSet.class);
参阅 EasyMock使用方法与原理剖析
EasyMock使用方法与原理剖析
easymock教程-参数匹配
【JUnit】EasyMock用法总结