第0章 概述
现在的dev不是仅仅要写code而已,UT已经变为开发中不可缺少的一环。JUnit的出现给javaer的UT编写提供了巨大的便利。但是JUnit并没有解决所有的问题。
当我们要测试一个功能点的时候,需要把不需要我们关注的东西隔离开,从而可以只关注我们需要关注的行为。
jmock通过mock对象来模拟一个对象的行为,从而隔离开我们不关心的其他对象,使得UT的编写变得更为可行,也使得TDD变得更为方便,自然而然的,也就成为敏捷开发的一个利器。
可以到http://www.jmock.org/download.html下载jmock.
添加jar到classpath。
添加的时候,注意把JUnit4的order放到最后。因为junit4它自己带了一个Hamcrest jar。
要是不注意顺序的话,有可能报
java.lang.SecurityException: class "org.hamcrest.TypeSafeMatcher"'s signer information does not match signer information of other classes in the same package。
Note:
这里的类定义用来演示如何使用jmock,所以都是定义为public的。
我们有一个UserManager,要测试它的方法,但是,UserManager是依赖于AddressService的。这里我们准备mock掉AddressService。
第1章 jmock初体验
这个例子的作用在于像一个传统的hello world一样,给大家一个简明的介绍,可以有一个感觉,jmock可以做什么。
AddressService本身太复杂,很难构建,这个时候,jmock出场了。
那么这里做了什么事情呢?
1 首先,我们建立一个test上下文对象。
2 用这个mockery context建立了一个mock对象来mock AddressService.
3 设置了这个mock AddressService的findAddress应该被调用1次,并且参数为"allen"。
4 生成UserManager对象,设置addressService,调用findAddress。
5 验证期望被满足。
基本上,一个简单的jmock应用大致就是这样一个流程。
最显著的优点就是,我们没有AddressService的具体实现,一样可以测试对AddressService接口有依赖的其他类的行为。也就是说,我们通过mock一个对象来隔离这个对象对要测试的代码的影响。
由于大致的流程是一样的,我们提供一个抽象类来模板化jmock的使用。
这样一来,我们以后的例子中只用关心setUpExpectatioin()和invokeAndVerify()方法就好了。
第2章 期望
好了,让我们来看看一个期望的框架。
invocation-count 调用的次数约束
mock-object mock对象
method 方法
argument-constraints 参数约束
inSequence 顺序
when 当mockery的状态为指定的时候触发。
will(action) 方法触发的动作
then 方法触发后设置mockery的状态
这个稍微复杂一些,一下子不明白是正常的,后面讲到其中的细节时,可以回来在看看这个框架。
第3章 返回值
调用一个方法,可以设置它的返回值。即设置will(action)。
这里演示了两种调用方法的结果,返回值和抛异常。
使用jmock可以返回常量值,也可以根据变量生成返回值。
抛异常是同样的,可以模拟在不同场景下抛的各种异常。
对于Iterator的返回值,jmock也提供了特殊支持。
从这个例子可以看到对于Iterator,returnValue和returnIterator的不同。
其实这里要返回一个Action,该Action负责返回调用的返回值。既然知道了这个道理,我们自然可以自定义Action来返回方法调用的结果。
而returnValue,returnIterator,throwException只不过是一些Expectations提供的一些static方法用来方便的构建不同的Action。
除了刚才介绍的
ReturnValueAction 直接返回结果
ThrowAction 抛出异常
ReturnIteratorAction 返回Iterator
还有
VoidAction
ReturnEnumerationAction 返回Enumeration
DoAllAction 所有的Action都执行,但是只返回最后一个Action的结果。
ActionSequence 每次调用返回其Actions列表中的下一个Action的结果。
CustomAction 一个抽象的Action,方便自定义Action。
举个例子来说明DoAllAction和ActionSequence的使用。
第4章 参数匹配
即设置argument-constraints
测试演示了直接匹配,equal匹配,自定义匹配,任意匹配。
其实,这些都是为了给参数指定一个Matcher,来决定调用方法的时候,是否接收这个参数。
在Expectations中提供了一些便利的方法方便我们构造Matcher.
其中
equal判断用equal方法判断是否相等。
same判断是否是同一个引用。
any,anything接收任意值。
aNull接收null。
aNonNull接收非null.
jmock提供了很多有用的匹配。可以用来扩展写出更多的Matcher。
基本Matcher
IsSame 引用相等。
IsNull
IsInstanceOf
IsEqual 考虑了数组的相等(长度相等,内容equals)
IsAnything always return true.
逻辑Matcher
IsNot
AnyOf
AllOf
其他
Is 装饰器模式的Matcher,使得可读性更高。
第5章 指定方法调用次数
可以指定方法调用的次数。即对invocation-count进行指定。
exactly 精确多少次
oneOf 精确1次
atLeast 至少多少次
between 一个范围
atMost 至多多少次
allowing 任意次
ignoring 忽略
never 从不执行
可以看出,这些range都是很明了的。只有allowing和ignoring比较特殊,这两个的实际效果是一样的,但是关注点不一样。当我们允许方法可以任意次调用时,用allowing,当我们不关心一个方法的调用时,用ignoring。
第6章 指定执行序列
这里指定了调用的序列。使得调用必须以指定的顺序调用。
来看一个反例
当指定序列的第一个调用没有触发的时候,直接调用第2个,则会抛异常。
Note:指定序列的时候注意方法调用次数这个约束,如果是allowing那么在这个序列中,它是可以被忽略的。
第7章 状态机
状态机的作用在于模拟对象在什么状态下调用才用触发。
可以看到,如果序列一样,状态也为期望的执行设置了约束,这里就是用状态来约束哪个期望应该被执行。
可以用is或者isNot来限制状态。
状态机有一个很好的用处。
当我们建立一个test执行上下文的时候,如果建立的时候和执行的时候,我们都需要调用mock ojbect的方法,那么我们可以用状态机把这两部分隔离开。让他们在不同的状态下执行。
现在的dev不是仅仅要写code而已,UT已经变为开发中不可缺少的一环。JUnit的出现给javaer的UT编写提供了巨大的便利。但是JUnit并没有解决所有的问题。
当我们要测试一个功能点的时候,需要把不需要我们关注的东西隔离开,从而可以只关注我们需要关注的行为。
jmock通过mock对象来模拟一个对象的行为,从而隔离开我们不关心的其他对象,使得UT的编写变得更为可行,也使得TDD变得更为方便,自然而然的,也就成为敏捷开发的一个利器。
可以到http://www.jmock.org/download.html下载jmock.
添加jar到classpath。
添加的时候,注意把JUnit4的order放到最后。因为junit4它自己带了一个Hamcrest jar。
要是不注意顺序的话,有可能报
java.lang.SecurityException: class "org.hamcrest.TypeSafeMatcher"'s signer information does not match signer information of other classes in the same package。
Note:
这里的类定义用来演示如何使用jmock,所以都是定义为public的。
- public class UserManager {
- public AddressService addressService;
- public Address findAddress(String userName) {
- return addressService.findAddress(userName);
- }
- public Iterator<Address> findAddresses(String userName) {
- return addressService.findAddresses(userName);
- }
- }
我们有一个UserManager,要测试它的方法,但是,UserManager是依赖于AddressService的。这里我们准备mock掉AddressService。
第1章 jmock初体验
这个例子的作用在于像一个传统的hello world一样,给大家一个简明的介绍,可以有一个感觉,jmock可以做什么。
AddressService本身太复杂,很难构建,这个时候,jmock出场了。
- @Test
- public void testFindAddress() {
- // 建立一个test上下文对象。
- Mockery context = new Mockery();
- // 生成一个mock对象
- final AddressService addressServcie = context
- .mock(AddressService.class);
- // 设置期望。
- context.checking(new Expectations() {
- {
- // 当参数为"allen"的时候,addressServcie对象的findAddress方法被调用一次,并且返回西安。
- oneOf(addressServcie).findAddress("allen");
- will(returnValue(Para.Xian));
- }
- });
- UserManager manager = new UserManager();
- // 设置mock对象
- manager.addressService = addressServcie;
- // 调用方法
- Address result = manager.findAddress("allen");
- // 验证结果
- Assert.assertEquals(Result.Xian, result);
- }
那么这里做了什么事情呢?
1 首先,我们建立一个test上下文对象。
2 用这个mockery context建立了一个mock对象来mock AddressService.
3 设置了这个mock AddressService的findAddress应该被调用1次,并且参数为"allen"。
4 生成UserManager对象,设置addressService,调用findAddress。
5 验证期望被满足。
基本上,一个简单的jmock应用大致就是这样一个流程。
最显著的优点就是,我们没有AddressService的具体实现,一样可以测试对AddressService接口有依赖的其他类的行为。也就是说,我们通过mock一个对象来隔离这个对象对要测试的代码的影响。
由于大致的流程是一样的,我们提供一个抽象类来模板化jmock的使用。
- public abstract class TestBase {
- // 建立一个test上下文对象。
- protected Mockery context = new Mockery();
- // 生成一个mock对象
- protected final AddressService addressServcie = context
- .mock(AddressService.class);
- /**
- * 要测试的userManager.
- * */
- protected UserManager manager;
- /**
- * 设置UserManager,并且设置mock的addressService。
- * */
- private void setUpUserManagerWithMockAddressService() {
- manager = new UserManager();
- // 设置mock对象
- manager.addressService = addressServcie;
- }
- /**
- * 调用findAddress,并且验证返回值。
- *
- * @param userName
- * userName
- * @param expected
- * 期望返回的地址。
- * */
- protected void assertFindAddress(String userName, Address expected) {
- Address address = manager.findAddress(userName);
- Assert.assertEquals(expected, address);
- }
- /**
- * 调用findAddress,并且验证方法抛出异常。
- * */
- protected void assertFindAddressFail(String userName) {
- try {
- manager.findAddress(userName);
- Assert.fail();
- } catch (Throwable t) {
- // Nothing to do.
- }
- }
- @Test
- public final void test() {
- setUpExpectatioin();
- setUpUserManagerWithMockAddressService();
- invokeAndVerify();
- }
- /**
- * 建立期望。
- * */
- protected abstract void setUpExpectatioin();
- /**
- * 调用方法并且验证结果。
- * */
- protected abstract void invokeAndVerify();
- }
这样一来,我们以后的例子中只用关心setUpExpectatioin()和invokeAndVerify()方法就好了。
第2章 期望
好了,让我们来看看一个期望的框架。
- 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的状态
这个稍微复杂一些,一下子不明白是正常的,后面讲到其中的细节时,可以回来在看看这个框架。
第3章 返回值
调用一个方法,可以设置它的返回值。即设置will(action)。
- @Override
- protected void setUpExpectatioin() {
- context.checking(new Expectations() {
- {
- // 当参数为"allen"的时候,addressServcie对象的findAddress方法返回一个Adress对象。
- allowing(addressServcie).findAddress("allen");
- will(returnValue(Para.BeiJing));
- // 当参数为null的时候,抛出IllegalArgumentException异常。
- allowing(addressServcie).findAddress(null);
- will(throwException(new IllegalArgumentException()));
- }
- });
- }
- @Override
- protected void invokeAndVerify() {
- assertFindAddress("allen", Result.BeiJing);
- assertFindAddressFail(null);
- }
这里演示了两种调用方法的结果,返回值和抛异常。
使用jmock可以返回常量值,也可以根据变量生成返回值。
抛异常是同样的,可以模拟在不同场景下抛的各种异常。
对于Iterator的返回值,jmock也提供了特殊支持。
- @Override
- protected void setUpExpectatioin() {
- // 生成地址列表
- final List<Address> addresses = new ArrayList<Address>();
- addresses.add(Para.Xian);
- addresses.add(Para.HangZhou);
- final Iterator<Address> iterator = addresses.iterator();
- // 设置期望。
- context.checking(new Expectations() {
- {
- // 当参数为"allen"的时候,addressServcie对象的findAddresses方法用returnvalue返回一个Iterator<Address>对象。
- allowing(addressServcie).findAddresses("allen");
- will(returnValue(iterator));
- // 当参数为"dandan"的时候,addressServcie对象的findAddresses方法用returnIterator返回一个Iterator<Address>对象。
- allowing(addressServcie).findAddresses("dandan");
- will(returnIterator(addresses));
- }
- });
- }
- @Override
- protected void invokeAndVerify() {
- Iterator<Address> resultIterator = null;
- // 第1次以"allen"调用方法
- resultIterator = manager.findAddresses("allen");
- // 断言返回的对象。
- assertIterator(resultIterator);
- // 第2次以"allen"调用方法,返回的与第一次一样的iterator结果对象,所以这里没有next了。
- resultIterator = manager.findAddresses("allen");
- Assert.assertFalse(resultIterator.hasNext());
- // 第1次以"dandan"调用方法
- resultIterator = manager.findAddresses("dandan");
- // 断言返回的对象。
- assertIterator(resultIterator);
- // 第2次以"dandan"调用方法,返回的是一个全新的iterator。
- resultIterator = manager.findAddresses("dandan");
- // 断言返回的对象。
- assertIterator(resultIterator);
- }
- /** 断言resultIterator中有两个期望的Address */
- private void assertIterator(Iterator<Address> resultIterator) {
- Address address = null;
- // 断言返回的对象。
- address = resultIterator.next();
- Assert.assertEquals(Result.Xian, address);
- address = resultIterator.next();
- Assert.assertEquals(Result.HangZhou, address);
- // 没有Address了。
- Assert.assertFalse(resultIterator.hasNext());
- }
从这个例子可以看到对于Iterator,returnValue和returnIterator的不同。
- @Override
- protected void setUpExpectatioin() {
- // 设置期望。
- context.checking(new Expectations() {
- {
- // 当参数为"allen"的时候,addressServcie对象的findAddress方法返回一个Adress对象。
- allowing(addressServcie).findAddress("allen");
- will(new Action() {
- @Override
- public Object invoke(Invocation invocation)
- throws Throwable {
- return Para.Xian;
- }
- @Override
- public void describeTo(Description description) {
- }
- });
- }
- });
- }
- @Override
- protected void invokeAndVerify() {
- assertFindAddress("allen", Result.Xian);
- }
其实这里要返回一个Action,该Action负责返回调用的返回值。既然知道了这个道理,我们自然可以自定义Action来返回方法调用的结果。
而returnValue,returnIterator,throwException只不过是一些Expectations提供的一些static方法用来方便的构建不同的Action。
除了刚才介绍的
ReturnValueAction 直接返回结果
ThrowAction 抛出异常
ReturnIteratorAction 返回Iterator
还有
VoidAction
ReturnEnumerationAction 返回Enumeration
DoAllAction 所有的Action都执行,但是只返回最后一个Action的结果。
ActionSequence 每次调用返回其Actions列表中的下一个Action的结果。
CustomAction 一个抽象的Action,方便自定义Action。
举个例子来说明DoAllAction和ActionSequence的使用。
- @Override
- protected void setUpExpectatioin() {
- // 设置期望。
- context.checking(new Expectations() {
- {
- // doAllAction
- allowing(addressServcie).findAddress("allen");
- will(doAll(returnValue(Para.Xian), returnValue(Para.HangZhou)));
- // ActionSequence
- allowing(addressServcie).findAddress("dandan");
- will(onConsecutiveCalls(returnValue(Para.Xian),
- returnValue(Para.HangZhou)));
- }
- });
- }
- @Override
- protected void invokeAndVerify() {
- assertFindAddress("allen", Result.HangZhou);
- assertFindAddress("dandan", Result.Xian);
- assertFindAddress("dandan", Result.HangZhou);
- }
第4章 参数匹配
即设置argument-constraints
- @Override
- protected void setUpExpectatioin() {
- // 设置期望。
- context.checking(new Expectations() {
- {
- // 当参数为"allen"的时候,addressServcie对象的findAddress方法返回一个Adress对象。
- allowing(addressServcie).findAddress("allen");
- will(returnValue(Para.Xian));
- // 当参数为"dandan"的时候,addressServcie对象的findAddress方法返回一个Adress对象。
- allowing(addressServcie).findAddress(with(equal("dandan")));
- will(returnValue(Para.HangZhou));
- // 当参数包含"zhi"的时候,addressServcie对象的findAddress方法返回一个Adress对象。
- allowing(addressServcie).findAddress(
- with(new BaseMatcher<String>() {
- @Override
- public boolean matches(Object item) {
- String value = (String) item;
- if (value == null)
- return false;
- return value.contains("zhi");
- }
- @Override
- public void describeTo(Description description) {
- }
- }));
- will(returnValue(Para.BeiJing));
- // 当参数为其他任何值的时候,addressServcie对象的findAddress方法返回一个Adress对象。
- allowing(addressServcie).findAddress(with(any(String.class)));
- will(returnValue(Para.ShangHai));
- }
- });
- }
- @Override
- protected void invokeAndVerify() {
- // 以"allen"调用方法
- assertFindAddress("allen", Result.Xian);
- // 以"dandan"调用方法
- assertFindAddress("dandan", Result.HangZhou);
- // 以包含"zhi"的参数调用方法
- assertFindAddress("abczhidef", Result.BeiJing);
- // 以任意一个字符串"abcdefg"调用方法
- assertFindAddress("abcdefg", Result.ShangHai);
- }
测试演示了直接匹配,equal匹配,自定义匹配,任意匹配。
其实,这些都是为了给参数指定一个Matcher,来决定调用方法的时候,是否接收这个参数。
在Expectations中提供了一些便利的方法方便我们构造Matcher.
其中
equal判断用equal方法判断是否相等。
same判断是否是同一个引用。
any,anything接收任意值。
aNull接收null。
aNonNull接收非null.
jmock提供了很多有用的匹配。可以用来扩展写出更多的Matcher。
基本Matcher
IsSame 引用相等。
IsNull
IsInstanceOf
IsEqual 考虑了数组的相等(长度相等,内容equals)
IsAnything always return true.
逻辑Matcher
IsNot
AnyOf
AllOf
其他
Is 装饰器模式的Matcher,使得可读性更高。
第5章 指定方法调用次数
可以指定方法调用的次数。即对invocation-count进行指定。
exactly 精确多少次
oneOf 精确1次
atLeast 至少多少次
between 一个范围
atMost 至多多少次
allowing 任意次
ignoring 忽略
never 从不执行
可以看出,这些range都是很明了的。只有allowing和ignoring比较特殊,这两个的实际效果是一样的,但是关注点不一样。当我们允许方法可以任意次调用时,用allowing,当我们不关心一个方法的调用时,用ignoring。
第6章 指定执行序列
- @Override
- protected void setUpExpectatioin() {
- final Sequence sequence = context.sequence("mySeq_01");
- // 设置期望。
- context.checking(new Expectations() {
- {
- // 当参数为"allen"的时候,addressServcie对象的findAddress方法返回一个Adress对象。
- oneOf(addressServcie).findAddress("allen");
- inSequence(sequence);
- will(returnValue(Para.Xian));
- // 当参数为"dandan"的时候,addressServcie对象的findAddress方法返回一个Adress对象。
- oneOf(addressServcie).findAddress("dandan");
- inSequence(sequence);
- will(returnValue(Para.HangZhou));
- }
- });
- }
- @Override
- protected void invokeAndVerify() {
- assertFindAddress("allen", Result.Xian);
- assertFindAddress("dandan", Result.HangZhou);
- }
这里指定了调用的序列。使得调用必须以指定的顺序调用。
来看一个反例
- @Override
- protected void setUpExpectatioin() {
- final Sequence sequence = context.sequence("mySeq_01");
- // 设置期望。
- context.checking(new Expectations() {
- {
- // 当参数为"allen"的时候,addressServcie对象的findAddress方法返回一个Adress对象。
- oneOf(addressServcie).findAddress("allen");
- inSequence(sequence);
- will(returnValue(Para.Xian));
- // 当参数为"dandan"的时候,addressServcie对象的findAddress方法返回一个Adress对象。
- oneOf(addressServcie).findAddress("dandan");
- inSequence(sequence);
- will(returnValue(Para.HangZhou));
- }
- });
- }
- @Override
- protected void invokeAndVerify() {
- assertFindAddressFail("dandan");
- }
当指定序列的第一个调用没有触发的时候,直接调用第2个,则会抛异常。
Note:指定序列的时候注意方法调用次数这个约束,如果是allowing那么在这个序列中,它是可以被忽略的。
第7章 状态机
状态机的作用在于模拟对象在什么状态下调用才用触发。
- @Override
- protected void setUpExpectatioin() {
- final States states = context.states("sm").startsAs("s1");
- // 设置期望。
- context.checking(new Expectations() {
- {
- // 状态为s1参数包含allen的时候返回西安
- allowing(addressServcie).findAddress(
- with(StringContains.containsString("allen")));
- when(states.is("s1"));
- will(returnValue(Para.Xian));
- // 状态为s1参数包含dandan的时候返回杭州,跳转到s2。
- allowing(addressServcie).findAddress(
- with(StringContains.containsString("dandan")));
- when(states.is("s1"));
- will(returnValue(Para.HangZhou));
- then(states.is("s2"));
- // 状态为s2参数包含allen的时候返回上海
- allowing(addressServcie).findAddress(
- with(StringContains.containsString("allen")));
- when(states.is("s2"));
- will(returnValue(Para.ShangHai));
- }
- });
- }
- @Override
- protected void invokeAndVerify() {
- // s1状态
- assertFindAddress("allen", Result.Xian);
- assertFindAddress("allen0", Result.Xian);
- // 状态跳转到 s2
- assertFindAddress("dandan", Result.HangZhou);
- // s2状态
- assertFindAddress("allen", Result.ShangHai);
- }
可以看到,如果序列一样,状态也为期望的执行设置了约束,这里就是用状态来约束哪个期望应该被执行。
可以用is或者isNot来限制状态。
状态机有一个很好的用处。
当我们建立一个test执行上下文的时候,如果建立的时候和执行的时候,我们都需要调用mock ojbect的方法,那么我们可以用状态机把这两部分隔离开。让他们在不同的状态下执行。