目录
JUnit的出现给javaer的UT编写提供了巨大的便利。但是JUnit并没有解决所有的问题。当我们要测试一个功能点的时候,需要把不需要我们关注的东西隔离开,从而可以只关注我们需要关注的行为。 mock和stub都可以用来对系统(或者将粒度放小为模块,单元)进行隔离。
目的相同,在测试,尤其是单元测试中,我们通常关注的是主要测试对象的功能和行为,对于主要测试对象涉及到的次要对象尤其是一些依赖,我们仅仅关注主要测试对象和次要测试对象的交互,比如是否调用,何时调用,调用的参数,调用的次数和顺序等,以及返回的结果或发生的异常。但次要对象是如何执行这次调用的具体细节,我们并不关注,因此常见的技巧就是用mock对象或者stub对象来替代真实的次要对象,模拟真实场景来进行对主要测试对象的测试工作。
1、类实现的方式
从类的实现方式上看,stub有一个显式的类实现,按照stub类的复用层次可以实现为普通类(被多个测试案例复用),内部类(被同一个测试案例的多个测试方法复用)乃至内部匿名类(只用于当前测试方法)。对于stub的方法也会有具体的实现,哪怕简单到只有一个简单的return语句。而mock则不同,mock的实现类通常是有mock的工具包如easymock, jmock来隐式实现,具体mock的方法的行为则通过record方式来指定。
对应代码:easymocktest工程:src\test\java\com\taobao\study\easymock\mockstub包下面
2、测试逻辑的可读性
mock通常是在测试代码中直接mock类和定义mock方法的行为,测试代码和mock的代码通常是放在一起的,因此测试代码的逻辑也容易从测试案例的代码上看出来。
stub的测试案例的代码中只有简单的UserDao userDao = new UserDaoStub ();构造语句和service.setUserDao(userDao);设置语句,我们无法直接从测试案例的代码中看出对依赖的预期,只能进入具体的UserServiceImpl类的query()方法,看到具体的实现是调用userDao.getById(userId),这个时候才能明白完整的测试逻辑。因此当测试逻辑复杂,stub数量多并且某些stub需要传入一些标记比如true,false之类的来制定不同的行为时,测试逻辑的可读性就会下降。
3. 可复用性
Mock通常很少考虑复用,每个mock对象通过都是遵循"just enough"原则,一般只适用于当前测试方法。因此每个测试方法都必须实现自己的mock逻辑,当然在同一个测试类中还是可以有一些简单的初始化逻辑可以复用。
stub则通常比较方便复用,尤其是一些通用的stub,比如jdbc连接之类。
4、设计和使用
从mock和stub的设计和使用上来比较两者,这里需要引入两个概念:interaction-based和state-based。具体关于interaction-based和state-based,强烈推荐Martin Fowler 的一篇文章,"Mocks Aren't Stubs"。地址为http://martinfowler.com/articles/mocksArentStubs.html总结来说,stub是state-based,关注的是输入和输出。mock是interaction-based,关注的是交互过程。
5、expectiation/期望
对于mock来说,exception是重中之重:我们期待方法有没有被调用,期待适当的参数,期待调用的次数,甚至期待多个mock之间的调用顺序。所有的一切期待都是事先准备好,在测试过程中和测试结束后验证是否和预期的一致。而对于stub,通常都不会关注exception,就像上面给出的UserDaoStub的例子,没有任何代码来帮助判断这个stub类是否被调用。
例子在easymocktest工程下面的com.taobao.study.easymock.chapter01包下的测试用例。
record-replay-verify 模型容许记录mock对象上的操作然后重演并验证这些操作。这是目前mock框架领域最常见的模型,几乎所有的mock框架都是用这个模型,有些是现实使用如easymock,有些是隐式使用如jmock。
1、record阶段:这里我们开始创建mock对象,并期望这个mock对象的方法被调用,同时给出我们希望这个方法返回的结果。这就是所谓的"记录mock对象上的操作", 同时我们也会看到"expect"这个关键字.总结说,在record阶段,我们需要给出的是我们对mock对象的一系列期望:若干个mock对象被调用,依从我们给定的参数,顺序,次数等,并返回预设好的结果(返回值或者异常).
2、replay阶段:在replay阶段,我们关注的主要测试对象将被创建,之前在record阶段创建的相关依赖被关联到主要测试对象,然后执行被测试的方法,以模拟真实运行环境下主要测试对象的行为。
3、verify阶段:在verify阶段,我们将验证测试的结果和交互行为。通常验证分为两部分,如上所示: 一部分是验证结果,即主要测试对象的测试方法返回的结果(对于异常测试场景则是抛出的异常)是否如预期,通常这个验证过程需要我们自行编码实现。另一部分是验证交互行为,典型如依赖是否被调用,调用的参数,顺序和次数,这部分的验证过程通常是由mock框架来自动完成,我们只需要简单调用即可。
1. 创建mock对象
UserDao userDao = Easymock.createMock(UserDao.class);
2. 记录mock对象期望的行为
Easymock.expect(userDao.getById("1001")).andReturn(expectedUser);这里记录了mock对象的行为:getById()方法被调用,调用次数为1(easymock之中如果没有明确指出调用次数,默认为1),参数为"1001",expectedUser将作为返回值。
3. 进入replay阶段
Easymock.replay(userDao);
4. 对mock对象执行验证
Easymock.verify(userDao);
以上四步就是我们所谓的全部步骤(ps:细节后边在细说)对上面的代码稍加改动以展示easymock的其他基本功能:
(1) 指定期望的调用次数
Easymock.expect(userDao.getById("1001")).andReturn(expectedUser).times(3);
(2)指定抛出期望的异常
Easymock.expect(userDao.getById("1001")).andThrow(new RuntimeException("no user exist"));
(3)记录void 方法的行为
Easymock.expect(userDao.getById("1001"))
这样的用法只能使用与mock对象的有返回值的方法,如果mock对象的方法是void,则需要使用
expectLastCall():
userDao.someVoidMethod();
Easymock.expectLastCall();
和Easymock.expect(***)一样,同样支持指定调用次数,抛出异常等:
Easymock.expectLastCall().times(3);
Easymock.expectLastCall().andThrow(new RuntimeException("some error"));
(4)灵活的参数匹配
Easymock.expect(userDao.getById(Easymock.isA(String.class))).andReturn(expectedUser);类似的还有anyInt(),anyObject(), isNull() , same(), startsWith()等诸多实现。
1、首先,我们建立一个test上下文对象。
Mockery context = new Mockery();
2、用这个mockery context建立了一个mock对象来mock AddressService. final AddressService addressServcie = context.mock(AddressService.class);
3、设置了这个mock AddressService的findAddress应该被调用1次,并且参数为"allen"。 context.checking(new Expectations() {
{
// 当参数为"allen"的时候,addressServcie对象的findAddress方法被调用一次,并且返回西安。
oneOf(addressServcie).findAddress("allen");
will(returnValue(Para.Xian));
}
});
4、生成UserManager对象,设置addressService,调用findAddress。
UserManager manager = new UserManager();
// 设置mock对象
manager.addressService = addressServcie;
// 调用方法
Address result = manager.findAddress("allen");
5、验证期望被满足。
Assert.assertEquals(Result.Xian, result);
注意:easy-mock可以模拟类并且可以部分模拟类,jmock在我进行验证测试的时候只能模拟接口,官方文档上也说可以。
1、easymock class extension的使用方式
和普通的interface mock完全一致,基本上easymock中有的功能easymock class extension都同样提供,而且所有的类名和方法名都保持一致。
2、class mocking是有一些限制的,
(1) 不能mock类的 final方法如果final方法被调用,则只能执行原有的正常代码。
(2) 不能mock类的static 方法。同样如果private方法被调用,只能执行原有的正常代码。
(3) 不能mock类的一些特殊方法: equals(), toString()和hashCode().原因是easymock在实现是为每个class mock对象提供了内建的以上三个方法。需要强调的是,对于基于interface的mock,这个限制也是同样存在的,即使以上三个方式是interface定义的一部分。
例子在com.taobao.study.easymock.limitation里面的测试用例
如果需要mock多个对象,则需要如此:
IMyInterface1 mock1 = createStrictMock(IMyInterface1.class);
IMyInterface2 mock2 = createStrictMock(IMyInterface2.class);
IMyInterface3 mock3 = createStrictMock(IMyInterface2.class);
...
replay(mock1, mock2, mock3, ...);
verify(mock1, mock2, mock3, ...);
reset(mock1, mock2, mock3, ...);
不仅需要为每个mock对象增加create语句,而且需要为这个新增的mock对象更新replay()/verify()/reset()方法,比较啰嗦,而且容易出错。这种情况下可以考虑使用MocksControl来简化代码:
IMocksControl mocksControl = createControl();
IMyInterface1 mock1 = mocksControl.createMock(IMyInterface1.class);
IMyInterface2 mock2 = mocksControl.createMock(IMyInterface2.class);
IMyInterface3 mock3 = mocksControl.createMock(IMyInterface3.class);
...
mocksControl.replay();
mocksControl.verify();
mocksControl.reset();
IMocksControl接口容许创建多个mock对象,这些创建的对象自动关联到这个mocksControl实例上,以后再调用replay()/verify()/reset()时就不需要逐个列举出每个mock对象。当mock对象比较多,尤其是原有代码上新增mock 对象时非常方便。
事实上,Easymock.createMock()方法内部实现也是使用IMocksControl的:
public static <T> T createMock(final Class<T> toMock) {
return createControl().createMock(toMock);
}
public static IMocksControl createControl() {
return new MocksControl(MocksControl.MockType.DEFAULT);
}
除了使用方便外,使用IMocksControl还有另外一个重要的好处,就是如果使用strict control,则可以跨多个mock对象检测方法的调用顺序。
例子在com.taobao.study.easymock.mockcontrol包里面测试用例
1、strict mock
strict mock方式下默认是开启调用顺序检测的,而普通的mock方式则默认不开启调用顺序检测。注意:EasyMock.createStrictMock()方法实际上内部是生成一个新的strict control,然后再创建mock对象。
Service1 service1 = EasyMock.createStrictMock("service1", Service1.class);
Service2 service2 = EasyMock.createStrictMock("service2", Service2.class);
这里实际是创建了两个strict control,而easymock是不会跨control进行顺序检测的。
2、nice mock
和createMock()相同的是默认不开启调用顺序检测,另外有一个非常有用的功能就是对于意料之外的调用将返回0,null 或者false.之所以说有用,是因为在我们的实际开发过程中,有时候会有这样的需求:对于某个mock对象的调用(可以是部分,也可以是全部),我们完全不介意调用细节,包括是否调用和调用顺序,参数,返回值,我们只要求mock对象容许程序可以继续而不是抛出异常报告说 unexpected invocations 。nicemock在这种情况下可以为我们节省大量的工作量,非常方便。
例子在com.taobao.study.easymock.strick报下面的测试用例
例子在com.taobao.study.easymock.nick报下面的测试用例
easymock中也提供有对stub的支持, 主要体现在
andStubAnswer(),andStubDelegateTo(),andStubReturn(),andStubThrow()和asStub()等方法的使用上。
一个正统的做法是手工写一个StubService的stub 实现目标接口,但是如果这个接口复杂方法众多,则这个stub类不得不实现所有的其他方法,即使完全用不到,因为java的语法限制。用andStubReturn()替代了andReturn().在适当的时候使用easymock来创建stub对象,对于简化测试还是能有所帮助的。
例子在com.taobao.study.easymock.stub包下面的测试用例
1、调用次数
对于mock对象上的mock方法的调用,easymock支持指定次数,默认为1
(1) once();如果明确调用次数为1,则可以使用这个方法显式指定,也可以省略,easymock默认为1。
(2) atLeastOnce();指定调用为1次或者多次,即 count >= 1.
(3)anyTimes();容许调用次数为任意次,即 count >= 0.
(4)times(int count);直接指定调用次数
(5)times(int min, int max);这个方法比较灵活,可以指定最小次数和最大次数。
2、利用调用次数改变同一方法调用的行为
expect(mock.voteForRemoval("Document"))
.andReturn((byte) 42).times(3)
.andThrow(new RuntimeException()).times(4)
.andReturn((byte) -42);
对于mock.voteForRemoval("Document")方法的调用,.andReturn((byte) 42).times(3) 表明前3次调用将返回42,.andThrow(new RuntimeException()).times(4)表示随后的4次调用(第4,5,6,7次)都将抛出异常,andReturn((byte) -42)表示第8次调用时将返回-42。
例子在com.taobao.study.easymock.specifytimes包下面的测试用例
1、基于基本类型的比较
(1) eq(X value)方法, X 可以是boolean,byte,char, double,float,int,long,short,T
(2) aryEq(X[] values) X 可以是boolean,byte,char, double,float,int,long,short,T
(3) gt(X value), lt(X value), X 可以是byte,double,float,int,long,short
(4) geq(X value), leq(X value)
(5) anyX(), X可以是Boolean, Byte, Char, Double, Float, Int, Long, Short
2、基于对象的比较
(1) eq(T value)方法和基本类型类似,不过对于Object,是通过调用equals()方法来进行比较。
(2) same(T value) 方法和eq()不同,same()是通过比较对象引用来进行比较的。类似java代码中, a.equals(b)和a == b的差别。
(3) anyObject() 和 anyObject(Class<T> clazz)类似基本类型的any***()方法,非常宽松,在我们不介意参数值时使用。
(4) 逻辑计算easymock支持在参数匹配时进行一些简单的逻辑计算, 如and(), or (), not()。
3、自定义参数匹配器
虽然easymock中提供了大量的方法来进行参数匹配,但是对于一些特殊场合比如参数是复杂对象而又不能简单的通过equals()方法来比较,这些现有的参数匹配器就无能为力了。easymock为此提供了IArgumentMatcher 接口来让我们实现自定义的参数匹配器.RequestMatcher 是我们定义的参数匹配器,matches()方法中是参数匹配逻辑的代码实现,appendTo()方法用于在匹配失败时打印错误信息,后面我们会演示这个方法的使用。然后是最重要的方法requestEquals(),在这里我们通过调用EasyMock.reportMatcher()告诉easymock我们要用的参数匹配器。
例子在com.taobao.study.easymock.customermatcher包下面的测试用例
4、利用capture验证参数是否正确
例子在easymocktest工程下面的ServiceTest测试用例进行。
有时候我们需要将这个测试类作为主要测试对象,我们希望这个类中的部分(通常是大部分)方法保持原有的正常行为,只有个别方法被我们mock掉以便测试。这里还需要注意一点,受限制的方法是不能被mock的。
例子在com.taobao.study.easymock.partialmock包下面的测试用例
easymock可以通过expect方法来设定mock方法的返回值或者异常,但是注意这些案例中设置的返回值都是在调用被测试的类的方法前就已经确定下来的,即我们其实在测试类的代码运行前(实际是在EasyMock.replay()方法调用前)就已经"预知"了返回结果。但是在某些情况下,我们可能无法预知返回值,比如我们需要根据输入的参数值来决定返回什么,而这个参数可能无法在record阶段获得。因此在mock方法中我们无法在record阶段就决定应该返回什么。对于这种场景,easymock提供了IAnswer接口和ndAnswer()方法来提供运行时决定返回值或者异常的机制。除了IAnswer接口外,easymock中还有另外一个方式可以完成类似的功能,就是使用andDelegate()方法,delegateTo方式实际上是我们手工创建了stub(mock和stub的概念及差别请参考本教程的"mock和stub"一文),这和我们使用easymock的初衷有所违背。而且当这个接口有众多方法时,创建这样一个stub会显得很痛苦,不如使用IAnswer方便直接。
例子在com.taobao.study.easymock.runtimecontext包下面的测试用例
一个期望的框架的语法格式如下所示。
invocation-count (mock-object).method(argument-constraints);
inSequence(sequence-name);
when(state-machine.is(state-name));
will(action);
then(state-machine.is(new-state-name));
其中各个参数的含义是:invocation-count à调用的次数约束;mock-object mockà对象 ;method à调用的方法 ;argument-constraints à参数约束 ;inSequence à顺序 ;when 当mockery的状态为指定的时候触发。 will(action) 方法触发的动作 then 方法触发后设置mockery的状态
调用一个方法,可以设置它的返回值。即设置will(action)。 这里面的action是关键,test-jmock工程里面的\test\com\taobao\jmock\returnvalue演示了两种调用方法的结果,返回值和抛异常。使用jmock可以返回常量值,也可以根据变量生成返回值(自定义Action利用invocation.getParameter来进行动态获取运行期传入的参数)。 抛异常是同样的,可以模拟在不同场景下抛的各种异常。除了刚才demo中有的ReturnValueAction 直接返回结果; ThrowAction 抛出异常; ReturnIteratorAction 返回Iterator ;其余的action有:VoidAction; ReturnEnumerationAction 返回Enumeration ;DoAllAction 所有的Action都执行,但是只返回最后一个Action的结果;ActionSequence 每次调用返回其Actions列表中的下一个Action的结果;CustomAction 一个抽象的Action,方便自定义Action(ActionTest)。ExampleActionTest演示了这一些Action的使用。
With语法是传入一个matcher进行验证参数是否合法,类似于Actoin,jmock已经默认的提供了很多的matcher,我们也可以自定义matcher,test-jmock下面的test\com\taobao\jmock\argumentconstraints包下的工程演示了各个matcher equal判断用equal方法判断是否相等。same判断是否是同一个引用。 any,anything接收任意值。 aNull接收null。 aNonNull接收非null. jmock提供了很多有用的匹配。可以用来扩展写出更多的Matcher。 基本Matcher IsSame 引用相等。 IsNull IsInstanceOf IsEqual 考虑了数组的相等(长度相等,内容equals) IsAnything always return true. 以及自定义matcher(实现BaseMatcher接口中的matches方法对传入的参数进行验证,其中要测试的辅助类如果有多个参数,请参照Argument02ConstraintsTest类,可以进行多参数的matcher验证)。
可以指定方法调用的次数。即对invocation-count进行指定。exactly 精确多少次 ;oneOf 精确1次 ;atLeast 至少多少次 ;between 一个范围 ;atMost 至多多少次 ;allowing 任意次 ;ignoring 忽略
never 从不执行 .可以看出,这些range都是很明了的。只有allowing和ignoring比较特殊,这两个的实际效果是一样的,但是关注点不一样。当我们允许方法可以任意次调用时,用allowing,当我们不关心一个方法的调用时,用ignoring。 代码在这个com.taobao.jmock.assigncalltimes包下面
这里指定了调用的序列。使得调用必须以指定的顺序调用。当指定序列的第一个调用没有触发的时候,直接调用第2个,则会抛异常。 Note:指定序列的时候注意方法调用次数这个约束,如果是allowing那么在这个序列中,它是可以被忽略的。并且注意: 不同的Sequence实例相对应在不同的Sequence组里面,不同的Sequence组的调用顺序可以使不同的。相应的代码见com.taobao.jmock.assigncallsequence包下面的程序。
(六) 状态机
状态机的作用在于模拟对象在什么状态下调用触发。相应的代码在com.taobao.jmock.statemachine包下面是对应的demo,状态机有一个很好的用处。当我们建立一个test执行上下文的时候,如果建立的时候和执行的时候,我们都需要调用mock ojbect的方法,那么我们可以用状态机把这两部分隔离开。让他们在不同的状态下执行。用处不是太多,但是有的地方用了就会起到画龙点睛的效果。
本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。