OCMock的学习

OCMock进行mock,以实现实现更加方便快捷的单元测试

2016-05-25 | 阅读

关于OCMock的学习

在写单元测试时,不可避免的要尽可能少地实例化一些具体的组件,来保持测试既短又快.我们会使用mock来替代实例化具体的依赖对象.mock是指在测试中用一个伪造的有预定义行为的具体对象的替身对象,而被测试的对象不知道两者的差异.官网点这里

本篇文章书写时,是基于OCMock的3.3版本的API的。

1. 创建Mock对象

  1. Class mocks

     id classMcok = OCMClassMock([SomeClass class]);
    

    创建一个mock对象,可以当做SomeClass的实例来使用.可以mock类的实例,也可以mock类以使用类方法,在下面将会提到.

  2. Protocol mocks

     id protocolMock = OCMProtocolMock(@protocol(SomeProtocol));
    

    类型mock类的对象一样,protocolMock可以像SomeProtocol的具体实现一样进行使用.

  3. Strict class and protocol mocks

     id classMock = OCMStrictClassMock([SomeClass class]);
     id protocolMock = OCMStrictProtocolMock(@protocol(SomeProtocol));
    

    创建一个严格的mock对象. mock对象会对将要执行的方法进行一定预测,如果没有设定预测,mock对象一般会返回nil或者返回类型的默认值.但严格的mock对象,规定了如果没有进行预测设定,而调用了这些方法,就会抛出异常,后面还会详细介绍.

  4. Partial mocks

     id partialMock = OCMPartialMock(anObject);
    

    创建一个anObject的mock对象,对于这个mock对象,如果设置了stub方法,对mock对象调用方法,mock对象会执行这个方法.如果对真实对象调用方法,依旧会执行mock对象的stub方法.而如果没有设置stub的方法,调用时会转发给真实对象. 下面会详解partial mocks

  5. Observer mocks

     id observerMock = OCMObserverMock();
    

    创建一个可以监听Notifications的mock,mock对象要注册监听的消息.下面会有详细介绍.

2. stubbing methods

stub方法,即对方法进行预测,如设置返回值,设置调用逻辑,抛出异常等等.设置stub后,调用函数时,原函数不再执行,而以stub的逻辑来进行交互.

  1. 返回对象

     OCMStub([mock someMethod]).andReturn(anObject);
    

    告诉mock对象,在someMethod方法调用时,以固定返回值返回.

  2. 返回非对象类型

     OCMStub([mock someMethod]).andReturn(YES);
    

    与返回对象类似,区别在于,如果传的值与方法的返回值类型不同,且不是简单可以替换的类型类型的话,如int和long的互换,则会抛出错误,

  3. 执行时调用其他方法

     OCMStub([mock someMethod]).andCall(anotherObject, @selector(aDifferentMethod));
    

    当mock对象的someMethod方法调用时,会调用anotherObject对象的aDifferentMethod方调用方法其实是替换实现,将mock方法调用时的参数传递给anotherObject对象的方法,并拿这个aDifferentMethod方法的返回值作为返回值返回.对于那些参数类型是对象类型或者其他类型的,可以用OCMArg的函数来忽略参数,即不处理参数,直接传递给aDifferentMethod函数

     + (id)any;
     + (SEL)anySelector;
     + (void *)anyPointer;
     + (id __autoreleasing *)anyObjectRef;
    

    但是对于参数类型中有非对象类型的参数,如基础数据类型int,Bool等,必须使用ignoringNonObjectArgs来传递参数了.

         // 对象中
         - (long)fbool:(long) b {
             return 2 + b;
         }
         // self
         - (long)fbool:(long)b {
             return 10 + b;
         }
         // 忽略参数,直接调用self的fbool函数
         [[[[partialMock stub] ignoringNonObjectArgs] andCall:@selector(fbool:) onObject:self] fbool:0];
         f = [partialMock fbool:12];
         // 最终输出为 22.
         NSLog(@"andCall ---- f == %@",@(f));		    
    
  4. 调用block

     OCMStub([mock someMethod]).andDo(^(NSInvocation *invocation)
     { /* block that handles the method invocation */ });
    

    在方法调用时,将参数以NSInvocation的形式进行封装,传递给block进行一些附加操作.由于传递的是NSInvocation,所以可以在block中来设置调用的返回值.

  5. 设置参数中的返回值

     OCMStub([mock someMethodWithReferenceArgument:[OCMArg setTo:anObject]]);
     OCMStub([mock someMethodWithReferenceArgument:[OCMArg setToValue:OCMOCK_VALUE((int){aValue})]]);
    

    这种mock的情况,是对于函数返回值放在参数中的情况,即参数传递的是一个 指针的指针,或者 void *指针的这种情况,直接不在执行mock函数,而是将返回值直接设置给返回值参数.所以这个函数的意思就是 将参数设置为一个对象或者一个值. 举简单的例子说明一下吧:

     // mock对象拥有这个函数.
     - (void)set:(NSString **)strptr {
         *strptr = @"hello world";
     }
    
     NSString *mocked = @"mocked";
     NSString *arguments;
     OCMStub([partialMock set:[OCMArg setTo:mocked]]);
       
     [partialMock set:&arguments];
        
     NSLog(@"arguments: %@" ,arguments );
     NSLog(@"mocked: %@" ,mocked );
    

    最终的输出是:

     arguments: mocked
     mocked: mocked
    

    set函数没有调用,直接将mocked的值设置给了arguments. 这种返回值在参数中的,一般用在 传递NSError的函数中.对于下面的值的设置,设置的值是基础类型而不是对象,如:

     - (void)set:(int *)kk {
         *kk = 12;
     }
    
     OCMStub([partialMock set:[OCMArg setToValue:OCMOCK_VALUE(11)]]);
    
  6. 执行block参数

     OCMStub([mock someMethodWithBlock:[OCMArg invokeBlock]]);
     OCMStub([mock someMethodWithBlock:([OCMArg invokeBlockWithArgs:@"First arg", nil])]);
    

    mock对象在调用stub方法时,传入一个block类型的参数, 这个时候进行stub,这个block将被执行.对于有参数的block,可以通过invokeBlockWithArgs来设置block的参数,如果没有设置参数,则以该类型的参数默认值传入参数,如NSString 是null,数值类型是0等.

     - (void)doBlock:(void (^)(NSString *)) blk{
         blk(@"doBlock");
         NSLog(@"doblock");
     }
    
     void (^argblk)(NSString *) = ^(NSString *str){
         NSLog(@"argblk -- %@",str);
     };
     // 这种情况下,传递的参数为空,输出日志为argblk -- (null)
     OCMStub([partialMock doBlock:[OCMArg invokeBlock]]);
     // 指定block的参数,输出日志为 argblk -- hello
     OCMStub([partialMock doBlock:([OCMArg invokeBlockWithArgs:@"hello", nil])]);
     [partialMock doBlock:argblk];
    
  7. 扔出一个异常

     OCMStub([mock someMethod]).andThrow(anException);
    
  8. 发送一个消息

     OCMStub([mock someMethod]).andPost(aNotification);
    
  9. 链式编程

     OCMStub([mock someMethod]).andPost(aNotification).andReturn(aValue);
    

    这里所有的操作都是可以链式传递的,所以调用函数时,可以同时执行block,发送消息,设定返回值等等.

  10. 消息传递给真实的对象

    OCMStub([mock someMethod]).andForwardToRealObject();
    

    只有在使用partial mock或者mock 类方法时,才能发消息给真的对象. 一般用在链式编程中,或者使用期望的时候.

  11. 什么也不做

    OCMStub([mock someMethod]).andDo(nil);
    

    注意,这个方法也只能在partial mock或者mock 类方法时使用. 传递一个空的block在函数调用时执行.

3. 验证相互调用

id mock = OCMClassMock([SomeClass class]);

/* run code under test */

OCMVerify([mock someMethod]);

使用 OCMVerify来判断函数是否执行过,可以在OCMVerify中添加参数判断,即判断函数以传入符合要求的参数被调用.如:

id mock = OCMClassMock([MC class]);
OCMStub([mock fbool:2]).andDo(^(NSInvocation *invocation){
    long x;
    [invocation getArgument:&x atIndex:2];
    NSLog(@"输入 - x - : %@",@(x));
});
[mock fbool:2];
OCMVerify([mock fbool:1]);

这里校验函数是否以参数为1的情况进行调用,当然这里没有,测试会失败.至于函数参数的约束,在下一节详细介绍:

4. 参数约束

参数约束,是指在符合约束的情况下,才会允许调用发送,否则会忽略调用.

  1. any ,不做约束

     OCMStub([mock someMethodWithAnArgument:[OCMArg any]])
     OCMStub([mock someMethodWithPointerArgument:[OCMArg anyPointer]])
     OCMStub([mock someMethodWithSelectorArgument:[OCMArg anySelector]])
    
  2. 非对象类型的忽略

     [[[mock stub] ignoringNonObjectArgs] someMethodWithIntArgument:0]
    

    这个时候,OCMock就没有提供宏给我们使用了,上面对于对象来说,可以设置一些参数忽略检测,一些参数进行检测,但是这个非对象类型,设置这个ignoringNonObjectArgs后,这个函数所有的非对象类型就全部被忽略了.

  3. 对象类型的检测

     OCMStub([mock someMethod:aValue)
     OCMStub([mock someMethod:[OCMArg isNil]])
     OCMStub([mock someMethod:[OCMArg isNotNil]])
     OCMStub([mock someMethod:[OCMArg isNotEqual:aValue]])
     OCMStub([mock someMethod:[OCMArg isKindOfClass:[SomeClass class]]])
     OCMStub([mock someMethod:[OCMArg checkWithSelector:aSelector onObject:anObject]])
     OCMStub([mock someMethod:[OCMArg checkWithBlock:^BOOL(id value) { /* return YES if value is ok */ }]])
    

    非对象类型,只能检测第一种情况了,即使指定值通过验证.而对象类型,可以有多种检测方式,还可以通过自己block或函数来校验参数值.

5. mock类方法

  1. Stubbing class methods

     id classMock = OCMClassMock([SomeClass class]);
     OCMStub([classMock aClassMethod]).andReturn(@"Test string");
    	
     // result is @"Test string"
     NSString *result = [SomeClass aClassMethod];
    

    mock类方法时,调用OCMClassMock,在这个函数中,动态创建了一个新的meta class,然后将SomeClass的指针指向新的meta class,这样做到 调用类方法时,被OCMock监控到.这也是mock类方法与partial mock对象的相似的地方,对原对象的调用,也会被stub方法截获.但是这里调用的时候就与其他mock对象不同了,是直接调用[SomeClass aClassMethod].

    注意 :如果一个类方法的mock对象stub了一个类方法,而mock对象没有正确释放,那这个方法的stub将一直持续下去.如果对于一个类同时存在多个mock对象,且stub了多个方法,会产生不可预知的后果. 使用OC的语言动态性来替换meta class,产生的一些危险.

  2. 验证方法是否调用

     OCMVerify([classMock aClassMethod]);
    

    调用mock函数,与执行校验,两种形式是不同的,要注意.

  3. 消除二义性

    有些情况下,类中会有同名的类方法和实例方法,这个时候要进行区分,使用ClassMethod

     OCMStub(ClassMethod([(MC *)mock fun])).andDo(^(NSInvocation *invocation){
         NSLog(@"BLOCK BLOCK ");
     });
     OCMStub([(MC *)mock fun]).andDo(^(NSInvocation *invocation){
         NSLog(@"block block ");
     });
     [MC fun];
     [(MC *)mock fun];
    

    这里使用[(MC *)mock fun]的原因是,Xcode区分不了要执行哪个函数,所以报错Multiple methods named 'fun' found with mismatched result,parameter type or attributes.

  4. 将mock的class恢复

     id classMock = OCMClassMock([SomeClass class]);
    	
     /* do stuff */
    	
     [classMock stopMocking];
    

    因为前面说到了,在mock类方法时,是直接替换类的meta class的,所以,要在测试完毕后,手动将类的meta class替换回去.调用stopMocking,将类恢复原样,mock对象在自己的dealloc中也会自动执行stopMocking的.

6. partial mocks

对象mock和类方法mock有很多相似的地方,因为原理相似,所以处理上也相似.

  1. stubbing methods

     id partialMock = OCMPartialMock(anObject);
     OCMStub([partialMock someMethod]).andReturn(@"Test string");
    	
     // result1 is @"Test string"
     NSString *result1 = [partialMock someMethod];
    	
     // result2 is @"Test string", too!
     NSString *result2 = [anObject someMethod];
    

    特点就是,对mock对象调用方法,和对原始对象调用方法,效果相同.而其实现原理,是创建一个子类,并将mock对象的类指向这个新生成的类.

  2. 验证调用

     // 验证方法调用时,也只有一种方法.
     OCMVerify([partialMock someMethod]);
    
  3. 将mock的对象恢复

     id partialMock = OCMPartialMock(anObject);
    	
     /* do stuff */
    	
     [partialMock stopMocking];
    

    将这个对象所对象的class恢复成正常的class.mock对象在自己的dealloc中也会自动执行stopMocking的.

7. 严格mock和预测

预测只是在预测函数是否调用.

  1. 预测函数是否调用

     id classMock = OCMClassMock([SomeClass class]);
     OCMExpect([classMock someMethodWithArgument:[OCMArg isNotNil]]);
    	
     /* run code under test, which is assumed to call someMethod */
    	
     OCMVerifyAll(classMock)
    

    OCMVerify是验证一条规则,OCMVerifyAll是验证所有规则.

  2. 严格的mock

     id classMock = OCMStrictClassMock([SomeClass class]);
     [classMock someMethod]; // this will throw an exception
    

    如果调用了没有mock的方法,测试就会失败.

  3. 预测返回值

     id classMock = OCMStrictClassMock([SomeClass class]);
     OCMExpect([classMock someMethod]).andReturn(@"a string for testing");
    	
     /* run code under test, which is assumed to call someMethod */
    	
     OCMVerifyAll(classMock)
    

    可以使用andReturnandThrow等来构建响应链.但事实上这种写法,还不如直接使用OCMVerify.

  4. 延迟预测

     OCMVerifyAllWithDelay(mock, aDelay);
    

    延迟一段时间进行预测.

  5. 设置一个预测序列

     id mock = OCMStrictClassMock([SomeClass class]);
     [mock setExpectationOrderMatters:YES];
     OCMExpect([mock someMethod]);
     OCMExpect([mock anotherMethod]);
    	
     // calling anotherMethod before someMethod will cause an exception to be thrown
     [mock anotherMethod];
    

    设置一个预测序列,必须按照序列进行函数调用,否则测试失败.

8. Observer mocks

// 创建一个observerMock
id observerMock = OCMObserverMock();
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
// 监听xxnotification 
[center addMockObserver:observerMock name:@"xxnotification" object:nil];
// 预测将要接受xxnotification
[[observerMock expect] notificationWithName:@"xxnotification" object:[OCMArg any]];
// 某个地方发送了消息
[center postNotificationName:@"xxnotification" object:nil];
// 预测完成且成功
OCMVerifyAll(observerMock);

预测mock对象将要收到SomeNotification消息,

9. 进阶话题

  1. 快速失败

    在严格的mock中,如果调用了没有mock的方法,就会失败.而正常的mock也可以设置这种快速失败的效果,通过OCMReject :

     id mock = OCMClassMock([SomeClass class]);
     OCMReject([mock someMethod]);
    
  2. 重新抛出Exception // 不只所云

    一个异常的抛出,不会导致测试失败.这会发生在方法调用没有在test函数内结束.

  3. Stub创建对象的方法

    OCMock可以stub方法返回对象.而且也会自动的调节返回对象的引用计数,所以可以放心的使用alloc,new,copymultableCopy方法.

     id classMock = OCMClassMock([SomeClass class]);
     OCMStub([classMock new])).andReturn(myObject);
    

    这里使用new,因为想要stub主 init方法是不可能的,这个方法已经被mock实现了,再替换会导致冲突.

  4. method swizzling

     id partialMock = OCMPartialMock(anObject);
     OCMStub([partialMock someMethod]).andCall(differentObject, @selector(differentMethod));
    

    andCall的方法就像是在method swizzling一样.但需要注意的是,partialMock中使用的是object_setClass来替换类,然后才在mockedClass中进行method swizzling.

10. 限制

  1. 在同一时间,只能有一个mock对象来stub一个类的类方法

     // don't do this
     id mock1 = OCMClassMock([SomeClass class]);
     OCMStub([mock1 aClassMethod]);
     id mock2 = OCMClassMock([SomeClass class]);
     OCMStub([mock2 anotherClassMethod]);
    
  2. 如果stub过了方法,就不能再用OCMExpect来预测了

     id mock = OCMStrictClassMock([SomeClass class]);
     OCMStub([mock someMethod]).andReturn(@"a string");
     OCMExpect([mock someMethod]);
    	
     /* run code under test */
    	
     OCMVerifyAll(mock); // will complain that someMethod has not been called
    

    因为这个OCMExpect本身就是在创建stub了,可以直接在OCMExpect之后直接链式接上andReturn等方法.

  3. 不能对一些特殊的类进行partial mock

     id partialMockForString = OCMPartialMock(@"Foo"); // will throw an exception
    	
     NSDate *date = [NSDate dateWithTimeIntervalSince1970:0];
     id partialMockForDate = OCMPartialMock(date); // will throw on some architectures
    

    首先不能对toll-free bridged的类进行partial mock,toll-free bridged指那些CoreFoundation,CoreGraphics等框架中可以与OC对象互相转换的类,如NSStringCFString. 以及 objects represented with tagged pointers ,如某些架构上的NSDate.

  4. 一些方法不能被stub和verify

     id partialMockForString = OCMPartialMock(anObject);
     OCMStub([partialMock class]).andReturn(someOtherClass); // will not work