jmock2.5基本教程

jmock2.5基本教程

目录
第0章 概述
第1章 jmock初体验
第2章 期望
第3章 返回值
第4章 参数匹配
第5章 指定方法调用次数
第6章 指定执行序列
第7章 状态机

第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的。

Java代码 收藏代码
  1. publicclassUserManager{
  2. publicAddressServiceaddressService;
  3. publicAddressfindAddress(StringuserName){
  4. returnaddressService.findAddress(userName);
  5. }
  6. publicIterator<Address>findAddresses(StringuserName){
  7. returnaddressService.findAddresses(userName);
  8. }
  9. }

我们有一个UserManager,要测试它的方法,但是,UserManager是依赖于AddressService的。这里我们准备mock掉AddressService。


第1章 jmock初体验

这个例子的作用在于像一个传统的hello world一样,给大家一个简明的介绍,可以有一个感觉,jmock可以做什么。
AddressService本身太复杂,很难构建,这个时候,jmock出场了。
Java代码 收藏代码
  1. @Test
  2. publicvoidtestFindAddress(){
  3. //建立一个test上下文对象。
  4. Mockerycontext=newMockery();
  5. //生成一个mock对象
  6. finalAddressServiceaddressServcie=context
  7. .mock(AddressService.class);
  8. //设置期望。
  9. context.checking(newExpectations(){
  10. {
  11. //当参数为"allen"的时候,addressServcie对象的findAddress方法被调用一次,并且返回西安。
  12. oneOf(addressServcie).findAddress("allen");
  13. will(returnValue(Para.Xian));
  14. }
  15. });
  16. UserManagermanager=newUserManager();
  17. //设置mock对象
  18. manager.addressService=addressServcie;
  19. //调用方法
  20. Addressresult=manager.findAddress("allen");
  21. //验证结果
  22. Assert.assertEquals(Result.Xian,result);
  23. }

那么这里做了什么事情呢?
1 首先,我们建立一个test上下文对象。
2 用这个mockery context建立了一个mock对象来mock AddressService.
3 设置了这个mock AddressService的findAddress应该被调用1次,并且参数为"allen"。
4 生成UserManager对象,设置addressService,调用findAddress。
5 验证期望被满足。

基本上,一个简单的jmock应用大致就是这样一个流程。

最显著的优点就是,我们没有AddressService的具体实现,一样可以测试对AddressService接口有依赖的其他类的行为。也就是说,我们通过mock一个对象来隔离这个对象对要测试的代码的影响。

由于大致的流程是一样的,我们提供一个抽象类来模板化jmock的使用。
Java代码 收藏代码
  1. publicabstractclassTestBase{
  2. //建立一个test上下文对象。
  3. protectedMockerycontext=newMockery();
  4. //生成一个mock对象
  5. protectedfinalAddressServiceaddressServcie=context
  6. .mock(AddressService.class);
  7. /**
  8. *要测试的userManager.
  9. **/
  10. protectedUserManagermanager;
  11. /**
  12. *设置UserManager,并且设置mock的addressService。
  13. **/
  14. privatevoidsetUpUserManagerWithMockAddressService(){
  15. manager=newUserManager();
  16. //设置mock对象
  17. manager.addressService=addressServcie;
  18. }
  19. /**
  20. *调用findAddress,并且验证返回值。
  21. *
  22. *@paramuserName
  23. *userName
  24. *@paramexpected
  25. *期望返回的地址。
  26. **/
  27. protectedvoidassertFindAddress(StringuserName,Addressexpected){
  28. Addressaddress=manager.findAddress(userName);
  29. Assert.assertEquals(expected,address);
  30. }
  31. /**
  32. *调用findAddress,并且验证方法抛出异常。
  33. **/
  34. protectedvoidassertFindAddressFail(StringuserName){
  35. try{
  36. manager.findAddress(userName);
  37. Assert.fail();
  38. }catch(Throwablet){
  39. //Nothingtodo.
  40. }
  41. }
  42. @Test
  43. publicfinalvoidtest(){
  44. setUpExpectatioin();
  45. setUpUserManagerWithMockAddressService();
  46. invokeAndVerify();
  47. }
  48. /**
  49. *建立期望。
  50. **/
  51. protectedabstractvoidsetUpExpectatioin();
  52. /**
  53. *调用方法并且验证结果。
  54. **/
  55. protectedabstractvoidinvokeAndVerify();
  56. }

这样一来,我们以后的例子中只用关心setUpExpectatioin()和invokeAndVerify()方法就好了。

第2章 期望

好了,让我们来看看一个期望的框架。
Java代码 收藏代码
  1. invocation-count(mock-object).method(argument-constraints);
  2. inSequence(sequence-name);
  3. when(state-machine.is(state-name));
  4. will(action);
  5. 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)。
Java代码 收藏代码
  1. @Override
  2. protectedvoidsetUpExpectatioin(){
  3. context.checking(newExpectations(){
  4. {
  5. //当参数为"allen"的时候,addressServcie对象的findAddress方法返回一个Adress对象。
  6. allowing(addressServcie).findAddress("allen");
  7. will(returnValue(Para.BeiJing));
  8. //当参数为null的时候,抛出IllegalArgumentException异常。
  9. allowing(addressServcie).findAddress(null);
  10. will(throwException(newIllegalArgumentException()));
  11. }
  12. });
  13. }
  14. @Override
  15. protectedvoidinvokeAndVerify(){
  16. assertFindAddress("allen",Result.BeiJing);
  17. assertFindAddressFail(null);
  18. }


这里演示了两种调用方法的结果,返回值和抛异常。
使用jmock可以返回常量值,也可以根据变量生成返回值。
抛异常是同样的,可以模拟在不同场景下抛的各种异常。

对于Iterator的返回值,jmock也提供了特殊支持。
Java代码 收藏代码
  1. @Override
  2. protectedvoidsetUpExpectatioin(){
  3. //生成地址列表
  4. finalList<Address>addresses=newArrayList<Address>();
  5. addresses.add(Para.Xian);
  6. addresses.add(Para.HangZhou);
  7. finalIterator<Address>iterator=addresses.iterator();
  8. //设置期望。
  9. context.checking(newExpectations(){
  10. {
  11. //当参数为"allen"的时候,addressServcie对象的findAddresses方法用returnvalue返回一个Iterator<Address>对象。
  12. allowing(addressServcie).findAddresses("allen");
  13. will(returnValue(iterator));
  14. //当参数为"dandan"的时候,addressServcie对象的findAddresses方法用returnIterator返回一个Iterator<Address>对象。
  15. allowing(addressServcie).findAddresses("dandan");
  16. will(returnIterator(addresses));
  17. }
  18. });
  19. }
  20. @Override
  21. protectedvoidinvokeAndVerify(){
  22. Iterator<Address>resultIterator=null;
  23. //第1次以"allen"调用方法
  24. resultIterator=manager.findAddresses("allen");
  25. //断言返回的对象。
  26. assertIterator(resultIterator);
  27. //第2次以"allen"调用方法,返回的与第一次一样的iterator结果对象,所以这里没有next了。
  28. resultIterator=manager.findAddresses("allen");
  29. Assert.assertFalse(resultIterator.hasNext());
  30. //第1次以"dandan"调用方法
  31. resultIterator=manager.findAddresses("dandan");
  32. //断言返回的对象。
  33. assertIterator(resultIterator);
  34. //第2次以"dandan"调用方法,返回的是一个全新的iterator。
  35. resultIterator=manager.findAddresses("dandan");
  36. //断言返回的对象。
  37. assertIterator(resultIterator);
  38. }
  39. /**断言resultIterator中有两个期望的Address*/
  40. privatevoidassertIterator(Iterator<Address>resultIterator){
  41. Addressaddress=null;
  42. //断言返回的对象。
  43. address=resultIterator.next();
  44. Assert.assertEquals(Result.Xian,address);
  45. address=resultIterator.next();
  46. Assert.assertEquals(Result.HangZhou,address);
  47. //没有Address了。
  48. Assert.assertFalse(resultIterator.hasNext());
  49. }

从这个例子可以看到对于Iterator,returnValue和returnIterator的不同。

Java代码 收藏代码
  1. @Override
  2. protectedvoidsetUpExpectatioin(){
  3. //设置期望。
  4. context.checking(newExpectations(){
  5. {
  6. //当参数为"allen"的时候,addressServcie对象的findAddress方法返回一个Adress对象。
  7. allowing(addressServcie).findAddress("allen");
  8. will(newAction(){
  9. @Override
  10. publicObjectinvoke(Invocationinvocation)
  11. throwsThrowable{
  12. returnPara.Xian;
  13. }
  14. @Override
  15. publicvoiddescribeTo(Descriptiondescription){
  16. }
  17. });
  18. }
  19. });
  20. }
  21. @Override
  22. protectedvoidinvokeAndVerify(){
  23. assertFindAddress("allen",Result.Xian);
  24. }


其实这里要返回一个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的使用。
Java代码 收藏代码
  1. @Override
  2. protectedvoidsetUpExpectatioin(){
  3. //设置期望。
  4. context.checking(newExpectations(){
  5. {
  6. //doAllAction
  7. allowing(addressServcie).findAddress("allen");
  8. will(doAll(returnValue(Para.Xian),returnValue(Para.HangZhou)));
  9. //ActionSequence
  10. allowing(addressServcie).findAddress("dandan");
  11. will(onConsecutiveCalls(returnValue(Para.Xian),
  12. returnValue(Para.HangZhou)));
  13. }
  14. });
  15. }
  16. @Override
  17. protectedvoidinvokeAndVerify(){
  18. assertFindAddress("allen",Result.HangZhou);
  19. assertFindAddress("dandan",Result.Xian);
  20. assertFindAddress("dandan",Result.HangZhou);
  21. }



第4章 参数匹配

即设置argument-constraints
Java代码 收藏代码
  1. @Override
  2. protectedvoidsetUpExpectatioin(){
  3. //设置期望。
  4. context.checking(newExpectations(){
  5. {
  6. //当参数为"allen"的时候,addressServcie对象的findAddress方法返回一个Adress对象。
  7. allowing(addressServcie).findAddress("allen");
  8. will(returnValue(Para.Xian));
  9. //当参数为"dandan"的时候,addressServcie对象的findAddress方法返回一个Adress对象。
  10. allowing(addressServcie).findAddress(with(equal("dandan")));
  11. will(returnValue(Para.HangZhou));
  12. //当参数包含"zhi"的时候,addressServcie对象的findAddress方法返回一个Adress对象。
  13. allowing(addressServcie).findAddress(
  14. with(newBaseMatcher<String>(){
  15. @Override
  16. publicbooleanmatches(Objectitem){
  17. Stringvalue=(String)item;
  18. if(value==null)
  19. returnfalse;
  20. returnvalue.contains("zhi");
  21. }
  22. @Override
  23. publicvoiddescribeTo(Descriptiondescription){
  24. }
  25. }));
  26. will(returnValue(Para.BeiJing));
  27. //当参数为其他任何值的时候,addressServcie对象的findAddress方法返回一个Adress对象。
  28. allowing(addressServcie).findAddress(with(any(String.class)));
  29. will(returnValue(Para.ShangHai));
  30. }
  31. });
  32. }
  33. @Override
  34. protectedvoidinvokeAndVerify(){
  35. //以"allen"调用方法
  36. assertFindAddress("allen",Result.Xian);
  37. //以"dandan"调用方法
  38. assertFindAddress("dandan",Result.HangZhou);
  39. //以包含"zhi"的参数调用方法
  40. assertFindAddress("abczhidef",Result.BeiJing);
  41. //以任意一个字符串"abcdefg"调用方法
  42. assertFindAddress("abcdefg",Result.ShangHai);
  43. }

测试演示了直接匹配,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章 指定执行序列
Java代码 收藏代码
  1. @Override
  2. protectedvoidsetUpExpectatioin(){
  3. finalSequencesequence=context.sequence("mySeq_01");
  4. //设置期望。
  5. context.checking(newExpectations(){
  6. {
  7. //当参数为"allen"的时候,addressServcie对象的findAddress方法返回一个Adress对象。
  8. oneOf(addressServcie).findAddress("allen");
  9. inSequence(sequence);
  10. will(returnValue(Para.Xian));
  11. //当参数为"dandan"的时候,addressServcie对象的findAddress方法返回一个Adress对象。
  12. oneOf(addressServcie).findAddress("dandan");
  13. inSequence(sequence);
  14. will(returnValue(Para.HangZhou));
  15. }
  16. });
  17. }
  18. @Override
  19. protectedvoidinvokeAndVerify(){
  20. assertFindAddress("allen",Result.Xian);
  21. assertFindAddress("dandan",Result.HangZhou);
  22. }

这里指定了调用的序列。使得调用必须以指定的顺序调用。
来看一个反例
Java代码 收藏代码
  1. @Override
  2. protectedvoidsetUpExpectatioin(){
  3. finalSequencesequence=context.sequence("mySeq_01");
  4. //设置期望。
  5. context.checking(newExpectations(){
  6. {
  7. //当参数为"allen"的时候,addressServcie对象的findAddress方法返回一个Adress对象。
  8. oneOf(addressServcie).findAddress("allen");
  9. inSequence(sequence);
  10. will(returnValue(Para.Xian));
  11. //当参数为"dandan"的时候,addressServcie对象的findAddress方法返回一个Adress对象。
  12. oneOf(addressServcie).findAddress("dandan");
  13. inSequence(sequence);
  14. will(returnValue(Para.HangZhou));
  15. }
  16. });
  17. }
  18. @Override
  19. protectedvoidinvokeAndVerify(){
  20. assertFindAddressFail("dandan");
  21. }

当指定序列的第一个调用没有触发的时候,直接调用第2个,则会抛异常。
Note:指定序列的时候注意方法调用次数这个约束,如果是allowing那么在这个序列中,它是可以被忽略的。


第7章 状态机
状态机的作用在于模拟对象在什么状态下调用才用触发。
Java代码 收藏代码
  1. @Override
  2. protectedvoidsetUpExpectatioin(){
  3. finalStatesstates=context.states("sm").startsAs("s1");
  4. //设置期望。
  5. context.checking(newExpectations(){
  6. {
  7. //状态为s1参数包含allen的时候返回西安
  8. allowing(addressServcie).findAddress(
  9. with(StringContains.containsString("allen")));
  10. when(states.is("s1"));
  11. will(returnValue(Para.Xian));
  12. //状态为s1参数包含dandan的时候返回杭州,跳转到s2。
  13. allowing(addressServcie).findAddress(
  14. with(StringContains.containsString("dandan")));
  15. when(states.is("s1"));
  16. will(returnValue(Para.HangZhou));
  17. then(states.is("s2"));
  18. //状态为s2参数包含allen的时候返回上海
  19. allowing(addressServcie).findAddress(
  20. with(StringContains.containsString("allen")));
  21. when(states.is("s2"));
  22. will(returnValue(Para.ShangHai));
  23. }
  24. });
  25. }
  26. @Override
  27. protectedvoidinvokeAndVerify(){
  28. //s1状态
  29. assertFindAddress("allen",Result.Xian);
  30. assertFindAddress("allen0",Result.Xian);
  31. //状态跳转到s2
  32. assertFindAddress("dandan",Result.HangZhou);
  33. //s2状态
  34. assertFindAddress("allen",Result.ShangHai);
  35. }

可以看到,如果序列一样,状态也为期望的执行设置了约束,这里就是用状态来约束哪个期望应该被执行。
可以用is或者isNot来限制状态。

状态机有一个很好的用处。
当我们建立一个test执行上下文的时候,如果建立的时候和执行的时候,我们都需要调用mock ojbect的方法,那么我们可以用状态机把这两部分隔离开。让他们在不同的状态下执行。

你可能感兴趣的:(jmock)