iOS单元测试之OCMock的简介和使用

一、OCMock简介

1.1、Mock介绍

作为一个动词,mock是模拟、模仿的意思;作为一个名词,mock是能够模仿真实对象行为的模拟对象。在软件测试中,mock所模拟的对象是什么呢?它一定不是我们所测试的对象,而是 SUT(Software Under Test:测试的对象) 的依赖(dependency)。换句话说,mock 的作用是模拟 SUT 依赖对象的行为。

文字不好理解,我们画个图,如下图所示,被测试对象是 A,A 依赖的是B,B 依赖的是 C。而我们要 mock 的是 B 的行为。图中 A 就是 SUT。

mock依赖关系.png

1.2、OCMock介绍

OCMock是一个用于为iOS或Mac OS X项目配置Mock测试的开源项目。

其实现思想就是根据要mock的对象的class来创建一个对应的对象,并且设置好该对象的属性和调用预定方法后的动作(例如返回一个值,调用代码块,发送消息等等),然后将其记录到一个数组中,接下来开发者主动调用该方法,最后做一个verify(验证),从而判断该方法是否被调用,或者调用过程中是否抛出异常等。

其实就是可以把它当做我们伪造的一个对象,我们给它一些预设的值之类的,然后就可以进行对应的验证了。

1.3、OCMock集成

项目集成 OCMock 第三方库,这个使用 pod 工具直接安装OCMock框架即可。若使用 iBiu 工具安装 OCMock 库需在 podfile 文件同级创建 Podfile.custom。

使用普通的 pod 文件相同格式添加 OCmock 如下:

source 'https://github.com/CocoaPods/Specs.git'
pod 'OCMock'

二、OCMock的入门

关于为什么需要 mock,OCMock 官网的 Introduction 举了以下一个例子(是个标准的 TDD 开发流程,值得学习一下):开发者需要开发一个从 Twitter 上拉取数据,然后更新用户界面的模块,如何应用 TDD 编写该模块的单元测试。接下来的内容,是根据 TDD 流程划分小节,关于 mock 的存在价值则分散在每个小节各处。

点击下载Demo:ZJHUnitTestDemo

2.1、示例模块划分

首先,划分大致模块,例如最简单的 MVC 模块划分方式,以确定接口。

Controller

@interface ZJHTwitterViewController : UIViewController
@property (nonatomic, strong) ZJHTwitterConnection *connection;
@property (nonatomic, strong) ZJHTweetView *tweetView;
- (void)updateTweetView;
@end

Data Source

@interface ZJHTwitterConnection : NSObject
// 检索新推文的方法。它返回一个ZJHTweetModel对象数组,如果无法处理请求,则返回nil。
- (NSArray <ZJHTweetModel *> *)fetchTweets;
@end

View

@interface ZJHTweetView : UIView
// 一个将单个推文添加到视图的方法
- (void)addTweet:(ZJHTweetModel *)aTweet;
@en

2.2、确定测试用例三要素

选定实现ZJHTwitterViewControllerupdateTweetView方法,该方法通过调用connection成员的fetchTweets获取 Twitter 数据,然后调用tweetView成员的addTweet:将数据显示到界面。TDD(Test Driven Development:测试驱动开发) 是测试先行,因此先编写针对updateTweetView方法的单元测试。在此之前,需要考虑如何处理ControllerViewConnection的依赖。试想,如果选择直接构建ViewConnection的实例,则开发者会面临以下问题(结合 F.I.R.S.T 原则考虑),主要来自于Connection

  • 使用真实的网络连接必然大大增加单元测试的运行时长,会违背 Fast 原则;

  • Twitter可能在任何时间点返回任何数据,这样会面临两种都很差的选择:

    • 1、在单个单元测试中处理各种响应情况,这样会使单元测试逻辑流程依赖于 Twitter 的具体响应数据,违背了 Isolated 原则;
    • 2、针对不同的响应数据编写不同的测试用例,但这样不能保证所有用例的断言都被执行到,而且不同的响应会执行到不同的断言,这样违背了 Repeatly 原则;
  • Twitter一般不会返回错误,如 404、500,而且也很难控制 Twitter 返回特定的错误,同时也违背了 Self-verifying 原则;

F.I.R.S.T 原则(参考优秀测试实践原则):

Fast — 测试应该能够被经常执行;
Isolated — 测试本身不能依赖于外部因素或其他测试的结果;
Repeatable — 每次运行测试都应该产生相同的结果;
Self-verifying — 测试应该依赖于断言,不需要人为干预;
Timely — 测试应该和生产代码一同书写

因此,在updateTweetView单元测试中直接构建所依赖的ViewConnection的实例是非常不明智的选择。于是 mock 便应运而生。Mock 是用于在模块的单元测试中,模拟 模块所依赖的对象的特定行为或特定数据的 替身。例如:可以指定 mock 对象的方法返回固定的目标数据(stubbing)、可以校验 mock 对象的方法是否有被触发(verifying)等等。Mock 可以使依赖的行为具备可确定、可编辑、可追踪特性

回到刚才的例子,由于不需要等待网络数据同步返回,而是直接由 mock 返回模拟数据,因此符合 Fast 原则;另外返回模拟数据高度可控,使之符合 Isolated、Repeatly、Self-verifying 原则。

既然有这么优秀的选择,那就可以正式着手编写测试用例了。接下来编写测试用例:Connection从 Twitter 拉取数据成功后,若Controller调用updateTweetViewView是否有刷新数据。首先需要明确单元测试用例的三个基本因素:

givenConnectionfetchTweet方法指定能返回 Twitter 数据;

whenController实例调用了updateTweetView

thenView是否有调用addTweet方法将 Twitter 数据显示到界面;

2.3、编写测试用例

由于测试的目标模块是Controller因此需要构建真实的实例,而依赖ConnectionTweetView则只需构建其 mock 替身,并为Controller所持有,此时Controller是不知道它们只是 mock 对象。由于 mock ConnectionfetchTweets操作的时间、数据不可确定性,所以需要给 fetchTweets 打桩(stub)返回固定的 Twitter 数据。当Controller实例调用updateTweetView方法时,需要验证(verify)mock TweetViewaddTweets:显示 Twitter 数据到界面的操作被触发。

@implementation ZJHTwitterViewControllerTests

- (void)testExample {
    //--------- Given Start ---------//
    // 1. 构建Controller实例
    ZJHTwitterViewController *controller = [ZJHTwitterViewController new];
    
    // 2. Mock一个ZJHTwitterConnection实例
    id mockConnection = OCMClassMock([ZJHTwitterConnection class]);
    controller.connection = mockConnection;
    
    // 创建一些数据
    ZJHTweetModel *testTweet1 = [ZJHTweetModel new];
    ZJHTweetModel *testTweet2 = [ZJHTweetModel new];
    NSArray *tweetArray = @[testTweet1, testTweet2];
    // 4. stub Connection 的 fetchTweets 方法使之固定返回Tweet模型数组
    OCMStub([mockConnection fetchTweets]).andReturn(tweetArray);
    
    // 4. Mock一个TweetView实例
    id mockView = OCMClassMock([ZJHTweetView class]);
    controller.tweetView = mockView;
    
    //--------- When Start ---------//
    // 5. 调用测试目标方法updateTweetView。里面会调用fetchTweets,然后会得到我们存根的数组tweetArray
    [controller updateTweetView];
    
    //--------- Then Start ---------//
    // 6. 验证 mock TweetView 的 addTweet: 显示Tweet到界面的操作被触发
    OCMVerify([mockView addTweet:[OCMArg any]]);
}

@end

2.4、编写实现代码

完成了updateTweetView方法的测试用例,就可以大致清楚updateTweetView需要处理什么数据(stub)、需要调用依赖的哪些方法(verify)。此时运行该测试用例必然不通过,因为还未实现updateTweetView。接下来开始实现updateTweetView。具体代码如下:

@implementation ZJHTwitterViewController

- (void)updateTweetView {
    NSArray *tweets = [self.connection fetchTweets];
    if (tweets != nil) { // 展示数据
        for (ZJHTweetModel *item in tweets) {
            [self.tweetView addTweet:item];
        }
    } else {  // 处理异常情况
    }
}

@end

此时运行测试用例,用例通过,因为满足了测试用例中的OCMVerify的条件:当(given)connection固定正常返回 Tweet 数据时,调用updateTweetView时(when),触发了tweetViewaddTweet:方法显示 Tweet 数据到界面。

三、OCMock的示例使用

3.1、生成 Mock 对象三种方式的对比

3.1.1、需要测试的代码

通过对Person类的talk方法进行测试举例,其中也涉及Men类以及Animaiton类,以下是三个类的相关源码。

MOPerson类:

@interface MOPerson()
@property(nonatomic,strong) MOMen *men;
@end

@implementation Person
- (void)talk:(NSString *)str {
    [self.men logstr:str];
    [MOAnimaiton logstr:str];
}
@end

MOMen类

@implementation MOMen
-(NSString *)logstr:(NSString *)str {
    NSLog(@"%@",str);
    return str;
}
@end

MOAnimaiton类

@implementation MOAnimaiton
+ (NSString *)logstr:(NSString *)str {
    NSLog(@"%@",str);
    return str;
}
@end
3.1.2、Nice Mock

NiceMock 创建的 mock 对象在进行方法测试时会优先调用实例方法,若未找到实例方法,会继续调用同名的类方法。因此该方法可以用来生成mock对象去测试类方法也可以测试对象方法。使用场景:Nice mock 是比较友好的,当一个没有存根的方法被调用时他不会引起一个异常会验证通过。如果你不想自己对很多的方法进行存根,那么使用 nice mock

- (void)testTalkNiceMock {
    MOPerson *person1 = [MOPerson new]; // 新建person类
    id mockA = OCMClassMock([MOMen class]); // mock一个Men对象
    person1.men = mockA;
    [person1 talk:@"123"];  // person类执行方法
    OCMVerify([mockA logstr:[OCMArg any]]); // 验证 logstr 方法有被调用
}
3.1.3、Strict Mock

使用方式:测试case如下,mockA是Strict Mock生成,要调用testTalkStrictMock方法,则该方法要使用stub进行存根,否则最后的OCMVerifyAll(mockA)就会抛出异常。使用场景:这种方式创建的 mock 对象,如果调用未 stub(stub 代表存根)的方法,会抛出一个异常。这需要保证在 mock 的生命周期中每一个独立调用的方法都是被存根的,这种方法使用比较严格,很少使用。

- (void)testTalkStrictMock {
    id mockA = OCMStrictClassMock([MOPerson class]); // StrictMock生成mockA
    OCMStub([mockA talk:[OCMArg any]]); // 使用stub进行存根
    [mockA talk:@"123"]; // 执行talk方法    
    OCMVerifyAll(mockA); // 验证mock方法有没有被执行
}
3.1.4、Partial Mock

这样创建的对象在调用方法时:如果方法被 stub,调用 stub 后的方法,如果方法没有被 stub,调用原来的对象的方法,该方法有限制只能 mock 实例对象。使用场景:当调用一个没有被存根的方法时,会调用实际对象的该方法。当不能很好的存根一个类的方法时,该技术是非常有用的。

- (void)testTalkPartialMock {
    MOPerson *person1 = [MOPerson new];
    MOMen *men = [MOMen new];
    id mockA = OCMPartialMock(men);
    // 如果方法被 stub,调用 stub 后的方法,如果方法没有被 stub,调用原来的对象的方法
//    OCMStub([mockA logstr:[OCMArg any]]).andReturn(@"456");;
    person1.men = mockA;
    [person1 talk:@"123"];
    OCMVerify([mockA logstr:[OCMArg any]]);
}

3.2、预期的验证

3.2.1、需要测试的代码
@implementation MOOCMockDemo

+ (void)handleLoadFinished:(NSDictionary *)info {
    MOPerson *person = [MOPerson personWithInfo:info];
    if ([person isValid]) {
        [self handleLoadSuccessWithPerson:person];
        [self showError:NO];
    } else {
        [self handleLoadFailWithPerson:person];
        [self showError:YES];
    }
}

+ (void)handleLoadSuccessWithPerson:(MOPerson *)person {
    // do something
}

+ (void)handleLoadFailWithPerson:(MOPerson *)person {
    // do something
}

+ (void)showError:(BOOL)error {
    // do something
}

@end
3.2.2、验证预期
- (void)testMockExpect {
    // 新建mock
    id mock = OCMClassMock([MOOCMockDemo class]);

    // 预期下列方法顺序执行
    [mock setExpectationOrderMatters:YES];
    
    // 预期 + 参数验证
    OCMExpect([mock handleLoadSuccessWithPerson:[OCMArg checkWithBlock:^BOOL(id obj) {
        MOPerson *person = (MOPerson *)obj;
        return [person.name isEqualToString:@"momo"];
    }]]);
    // 预期方法
    OCMExpect([mock showError:NO]);

    // 预期不执行
    OCMReject([mock handleLoadFailWithPerson:[OCMArg any]]);
    OCMReject([mock showError:YES]).ignoringNonObjectArgs; // 忽视参数

    // 执行方法
    NSDictionary *info = @{@"name": @"momo"};
    [MOOCMockDemo handleLoadFinished:info];
    
    // 断言
    OCMVerifyAll(mock); // OCMVerifyAll 会验证前面的期望是否有效,只要有一个没调用,就会出错。
    OCMVerifyAllWithDelay(mock, 1); // 支持延迟验证
    
    // 停止Mocking
    [mock stopMocking];
}

3.3、网络接口的模拟

3.3.1、需要测试的代码
@implementation ZJHOrderListViewController

- (void)getListData { // 接口请求获取网络数据
    [ZJHNetworkTool requestUrl:@"url" param:@{} completion:^(NSDictionary * _Nonnull respondDic) {
        self.dataArr = respondDic[@"data"];
        [self refreshView];
    }];
}

- (void)refreshView { // 刷新页面
    NSLog(@"***ZJH refreshView : %@", self.dataArr);
}

@end
3.3.2、网络接口模拟
- (void)testMockNetwork {
    ZJHOrderListViewController *listVc = [ZJHOrderListViewController new];
    
    id mockManager = OCMClassMock([ZJHNetworkTool class]);
    // mock请求方法,并返回特定参数
    OCMStub([mockManager requestUrl:[OCMArg any] param:[OCMArg any] completion:[OCMArg any]]).andDo(^(NSInvocation *invocation){
        
        void (^successBlock)(NSDictionary *respondDic) = nil;
        
        [invocation getArgument:&successBlock atIndex:4];
        
        successBlock( @{ @"data" : @[@"a", @"b", @"c"] } );
    });
  
    [listVc getListData];
    
    OCMVerifyAll(mockManager);
}

以上就是在调用 getListData 方法内部调用了接口,该方法就可以在调用接口后模拟需要的返回数据,successBlock 中的就是返回的测试数据。本方式是通过获取接口调用的方法签名,获取 successBlock 成功回调传参并手动调用。同样可以模拟接口失败的情况,只需获取到签名中的对应的失败回调就可以实现了。使用场景:书写单元测试方法时涉及网络接口的模拟,通过该方式 mock 接口返回结果。

四、OCMock基本API详解

本章根据官方文档 Documentation 改编而来,可以在里查看如何详细使用OCMock。

4.1、创建模拟对象 Creating mock objects

4.1.1、模拟实例 Class mocks
// 根据类,模拟其实例
id mockPerson = OCMClassMock([MOPerson class]);
4.1.2、模拟代理 Protocol mocks
// 根据协议名,模拟已经实现协议的实例
id mockProtocol = OCMProtocolMock(@protocol(MOTitleLineViewDelegate));
// 然后mock协议方法
4.1.3、严格模拟类和协议 Strict class and protocol mocks
// 在收到没有预期(expect)的方法时引发异常
id strictMockClass = OCMStrictClassMock([MOPerson class]);
id strictMockProtocol = OCMStrictProtocolMock(@protocol(MOTitleLineViewDelegate));
4.1.4、部分模拟 Partial mocks

这里介绍一个定义:Stub,存根,就是模拟一个函数。

MOPerson *aPerson = [[MOPerson alloc] init];
id partialMockPerson = OCMPartialMock(aPerson);

调用一个函数:已经存根的就触发存根的(Stub);未存根的就触发原有实例的(aPerson)。

4.1.5、观察者模拟 Observer mocks

用官方的XCTNSNotificationExpectation

4.2、存根方法 Stubbing methods

4.2.1、模拟方法的返回值 Stubbing methods that return objects
OCMStub([partialMockPerson name]).andReturn(@"moxiaoyan"); 
OCMStub([mock aMethodReturningABoolean]).andReturn(YES);
4.2.2、委托给另一个方法 Stubbing methods that return values
MOPerson *anotherPerson = [[MOPerson alloc] init];
// 另一个对象的方法,方法签名需要一致
OCMStub([partialMockPerson name]).andCall(anotherPerson, @selector(name));
4.2.3、委托给一个block Delegating to another method
OCMStub([partialMockPerson name]).andDo(^(NSInvocation *invocation){
    // 调用name方法时,将会调用这个block
    // invocation会携带方法参数
    // invocation可以设置返回值
});
OCMStub([partialMock name]).andDo(nil);
4.2.4、委托给块 Delegating to a block

模拟对象将在调用函数时,调用该Block。该Block可以从调用的对象中读取参数,并可以设置返回值。

OCMStub([mock someMethod]).andDo(^(NSInvocation *invocation) {
    /* block that handles the method invocation */
});
4.2.5、模拟 通过参数返回值的方法 的返回值 Returning values in pass-by-reference arguments
4.2.5.1、对象参数

通过参数传回值:

// 模拟 应该返回的参数值
NSError *error = [NSError errorWithDomain:@"获取friends失败(stubbed)" code:001 userInfo:nil];
OCMStub([partialMockPerson loadFriendsWithError:[OCMArg setTo:error]]);
// 函数调用,获得模拟的值
NSError *resultError = nil;
[partialMockPerson loadFriendsWithError:&resultError];
NSLog(@"%@", resultError); // 001, 获取friends失败(stubbed)
4.2.5.2、非对象参数
OCMStub([mock someMethodWithReferenceArgument:[OCMArg setToValue:OCMOCK_VALUE((int){aValue})]]);
4.2.6、模拟block参数 Invoking block arguments
// invokeBlock默认模拟,参数都为默认值
OCMStub([partialMockPerson deviceWithComplete:[OCMArg invokeBlock]]);
[partialMockPerson deviceWithComplete:^(NSString * _Nonnull value) {
    NSLog(@"%@", value); // nil
}];
// invokeBlockWithArgs模拟,可以设置参数值
OCMStub([partialMockPerson deviceWithComplete:[OCMArg invokeBlockWithArgs:@"iPhone"]]);
[partialMockPerson deviceWithComplete:^(NSString * _Nonnull value) {
    NSLog(@"%@", value); // iPhone
}];
4.2.7、抛出异常 Throwing exceptions

设置函数被调用时,抛出异常:

NSException *exception = [[NSException alloc] initWithName:@"获取name异常" reason:@"name为空" userInfo:nil];
OCMStub([partialMockPerson name]).andThrow(exception);
4.2.8、发出通知 Posting notifications

设置函数被调用是,发出通知(notify

NSNotification *notify = [NSNotification notificationWithName:@"通知" object:self userInfo:nil];
OCMStub([partialMockPerson name]).andPost(notify);
4.2.9、链接模拟方法 Chaining stub actions

诸如andReturn和 之类的所有操作andPost都可以链接

// 模拟对象将发布通知并返回值
OCMStub([mock someMethod]).andPost(aNotification).andReturn(aValue);
4.2.10、转发给真正的对象/类 Forwarding to the real object / class

当使用部分模拟实例和模拟类方法时,可以将存根方法转发给真实对象或类。这仅在链接操作或使用期望时有用。

OCMStub([partialMockPerson name]).andForwardToRealObject();
4.2.11、什么也不做 Doing nothing

可以将nil而不是块传递给andDo。这仅在部分模拟或模拟类方法时有用。在这些情况下,使用andDo(nil)有效地抑制了现有类中的行为。

OCMStub([mock someMethod]).andDo(nil);
4.2.12、满足XCTest的期望(需要OCMock3.8)Fulfilling XCTest expectations

当调用该方法时,XCTest 框架中的期望得到满足:

XCTestExpectation *expectation = [[XCTestExpectation alloc] initWithDescription:@"XCTest的期望"];
OCMStub([partialMockPerson name]).andFulfill(expectation);
4.2.13、记录消息(需要OCMock3.8)Logging messages
OCMStub([partialMockPerson name]).andLog(@"%@", @"hehe");

调用该方法时,format通过NSLog。很可能您想在一个链中使用它,可能后跟andReturn()andForwardToRealObject()

4.2.14、打开调试,断点会生效(需要OCMock3.8)
OCMStub([partialMockPerson name]).andBreak();

当调用该方法时,调试器被打开,就好像一个断点被命中一样。堆栈将在 OCMock 的实现中的某个地方结束,但是如果您进一步查看,越过__forwarding__帧,您应该能够看到您的代码调用该方法的位置。

4.3、验证交互 Verifying interactions

4.3.1、验证方法已调用 Verify-after-running
[aPerson name];
OCMVerify([partialMockPerson name]);

验证name已被测试代码调用。如果尚未调用该方法,则会报告错误。

4.3.2、验证Stubbed的方法被调用 Stubs and verification
OCMStub([partialMockPerson name]).andReturn(@"momo");
[aPerson name];
OCMVerify([partialMockPerson name]);

可以存根一个方法并仍然验证它是否已被调用。

4.3.3、量词要求 Quantifiers requires

验证方法被调用的次数:

OCMVerify(atLeast(2), [partialMockPerson name]);
OCMVerify(never(),    [partialMock doStuff]);
OCMVerify(times(0),   [partialMock doStuff]);
OCMVerify(times(n),   [partialMock doStuff]);
OCMVerify(atLeast(n), [partialMock doStuff]);
OCMVerify(atMost(n),  [partialMock doStuff]);

4.4、参数约束 Argument constraints

4.4.1、任何约束 The any constraint
// stub方法,可以响应任何调用
OCMStub([partialMockPerson addChilden:[OCMArg any]]); // 参数是任何对象
OCMStub([partialMockPerson takeMoney:[OCMArg anyPointer]]); // 参数是任何指针
OCMStub([partialMockPerson changeWithSelector:[OCMArg anySelector]]); // 参数是任何选择子
4.4.2、忽视没有对象参数 Ignoring non-object arguments

stub方法,可以响应非对象参数的调用(可以响应参数没有通过的调用:无论是对象参数 or 非对象参数)

OCMStub([partialMockPerson setAge:0]).ignoringNonObjectArgs();
4.4.3、匹配参数 Matching arguments

stub方法,仅响应匹配的参数的调用

MOPerson *bPerson = [[MOPerson alloc] init];
OCMStub([partialMockPerson addChilden:bPerson]);
OCMStub([partialMockPerson addChilden:[OCMArg isNil]]);
OCMStub([partialMockPerson addChilden:[OCMArg isNotNil]]);
OCMStub([partialMockPerson addChilden:[OCMArg isNotEqual:bPerson]]);
OCMStub([partialMockPerson addChilden:[OCMArg isKindOfClass:[MOPerson class]]]);

会触发 anObjectaSelector 方法,并将参数传入
在该方法中判断参数是否通过,通过就:返回YES, 否则:返回NO

id anObject = nil;
SEL aSelector = @selector(addChilden:);
OCMStub([partialMockPerson addChilden:[OCMArg checkWithSelector:aSelector onObject:anObject]]);

OCMStub([partialMockPerson addChilden:[OCMArg checkWithBlock:^BOOL(id value) {
    // 判断参数是否通过,通过就:返回YES, 否则:返回NO
    return YES;
}]]);
4.4.4、使用Hamcrest匹配
OCMStub([partialMockPerson addChilden:startsWith(@"foo")]);

4.5、模拟类方法 Mocking class methods

4.5.1、存根类方法 Stubbing class methods
id mockPerson = OCMClassMock([MOPerson class]);
OCMStub([mockPerson mo_className]).andReturn(@"XXMOPerson");
4.5.2、消除类和实例方法的歧义 Disambiguating class and instance methods
// (1)此时如果没有同名的实例方法,mo_className类方法是可以被正确Stub的
NSString *className1 = [MOPerson mo_className]; // XXMOPerson
// (2)但是如果实例方法有跟之同名时:
NSString *instanceName = [mockPerson mo_className]; // XXMOPerson
NSString *className2 = [MOPerson mo_className]; // class MOPerson
// 则需要用一下方法进行Stub
OCMStub(ClassMethod([mockPerson mo_className])).andReturn(@"MOMOPerson");
NSString *className3 = [MOPerson mo_className]; // XXMOPerson
4.5.3、验证类方法已执行 Verifying invocations of class methods
[mockPerson mo_className];
OCMVerify([mockPerson mo_className]);
4.5.4、恢复类 Disambiguating class and instance methods
[mockPerson stopMocking];

4.6、部分模拟 Partial mocks

4.6.1、存根方法 Stubbing methods
id partialMockPerson = OCMPartialMock(aPerson);
OCMStub([partialMockPerson mo_className]).andReturn(@"Partail Class");
NSString *partialName = [partialMockPerson mo_className]; // Partail Class
NSString *personName = [aPerson mo_className]; // Partail Class
4.6.2、验证调用 Verifying invocations
[partialMockPerson mo_className];
OCMVerify([partialMockPerson mo_className]);
4.6.3、恢复对象 Restoring the object
[partialMockPerson stopMocking];

4.7、严格的模拟和期望 Strict mocks and expectations

4.7.1、设置期望-运行-验证 Expect-run-verify
id mockPerson = OCMClassMock([MOPerson class]);
OCMExpect([mockPerson addChilden:[OCMArg isNotNil]]);
[mockPerson addChilden:[MOPerson new]]; // 只要有一次不为nil,就通过了验证!
[mockPerson addChilden:nil];
OCMVerifyAll(mockPerson);
4.7.2、严格的模拟和快速失败 Strict mocks and failing fast
id strictPerson = OCMStrictClassMock([MOPerson class]);
[strictPerson mo_className]; // 没有期望该方法的调用,所以会测试失败
4.7.3、存根和期望 Stub actions and expect

也可以在期望的情况下使用andReturnandThrow等。这将在调用方法时运行存根操作,并在验证时确保该方法被实际调用

OCMExpect([strictPerson mo_className]).andReturn(@"instance_MOPerson");
OCMExpect([strictPerson mo_className]).andThrow([NSException ...]);
[strictPerson mo_className];
OCMVerifyAll(strictPerson);
4.7.4、延迟验证 Verify with delay
OCMExpect([strictPerson mo_className]);
[strictPerson mo_className];
OCMVerifyAllWithDelay(strictPerson, 4.0); // NSTimeInterval, 通常会在满足预期后立即返回
4.7.5、按顺序验证 Verifying in order

一旦调用了不在“预期列表”中的下一个方法,模拟就会快速失败并抛出异常。

[strictPerson setExpectationOrderMatters:YES];
OCMExpect([strictPerson mo_className]);
OCMExpect([strictPerson addChilden:[OCMArg any]]);
// 调用顺序错了,测试就会失败
[strictPerson mo_className];
[strictPerson addChilden:nil];

4.8、观察者模拟 Observer mocks

OCMock 4.8开始不推荐使用观察者模拟。请改用XCTNSNotificationExpectation

4.9、进阶主题 Advanced topics

4.9.1、快速失败的常规模拟 (需要OCMock3.3) Failing fast for regular (nice) mocks

strict模拟:调用未存根的方法会抛出异常
常规模拟:只是返回默认值;可以为函数配置快速失败:

id mockPerson = OCMClassMock([MOPerson class]);
OCMReject([mockPerson mo_className]);

在这种情况下,模拟将接受所有方法,除了mo_className,如果调用该函数,则将引发异常。

4.9.2、重新验证失败后快速抛出异常 Re-throwing fail fast exceptions in verify all

在快速失败模式下,异常可能不会导致测试失败(如:当方法的调用堆栈未在测试中结束时)
OCMerifyAll调用时,快速失败异常将重新引发,可以确保检测到来自通知等不需要的调用

4.9.3、存根创建对象的方法 Stubbing methods that create objects
MOPerson *myPerson = [[MOPerson alloc] init];
OCMStub([mockPerson copy]).andReturn(myPerson);

会根据方法名,自动返回对象的:allocnewcopymutableCopy (引用计数)

注意:init方法无法Stub,因为该方法是由模拟本身实现的。 当init方法再次被调用时,会直接返回模拟对象self
这样就可以有效的对alloc、init进行Stub

4.9.4、基于实现的方法交换 Instance-based method swizzling
MOPerson *person = [[MOPerson alloc] init];
id partialMockPerson = OCMPartialMock(person);
OCMStub([partialMockPerson mo_className]).andCall(myPerson, @selector(name));

方法的名称可以不同,但是签名应该相同

4.9.5、打破保留周期 Breaking retain cycles
[mockPerson stopMocking];
[partialMockPerson stopMocking];
4.9.6、禁用短语法 Disabling short syntax

禁用 没有前缀的宏:ClassMethod()atLeast()、…
用有前缀的宏:OCMClassMethod()OCMAtLeast()、…

4.9.7、停止为特定类创建模拟 (需要OCMock3.8) Stopping creation of mocks for specific classes

一些框架在运行时动态更改对象的类。OCMock这样做是为了实现部分模拟,并且Foundation框架将更改类作为(KVO)机制的一部分。
如果不仔细协调,可能会导致意外行为或crash

OCMock知道KVO,并小心避免与之发生冲突
对于其它框架,OCMock仅提供了一种选择退出模拟以免发生意外行为的机制

+ (BOOL)supportsMocking:(NSString **)reason {
    *reason = @"Don't want to be mocked."
    return NO;
}

通过实现上面的方法,一个类可以选择不被Mock。当开发人员尝试为此类创建模拟程序时,将引发异常,解释问题说在该方法在单独调用中返回不同的值是可以接受的,这使它在运行时对特定条件做出反应。如果该方法为reason赋值,返回值将被忽略。对于所有未实现此方法的类,OCMock假定可以接受Mock

4.9.8、检查部分Mock (需要OCMock3.8) Checking for partial mock

判断是否 是部分模拟对象

BOOL isPartialMockObj = OCMIsSubclassOfMockClass(objc_getClass(partialMockPerson));

4.10、局限性 Limitations

4.10.1、一次只能有一个Mock可以在给定类上存根方法

不要这样做:

id mock1 = OCMClassMock([SomeClass class]);
OCMStub([mock1 aClassMethod]);
id mock2 = OCMClassMock([SomeClass class]);
OCMStub([mock2 anotherClassMethod]);

如果添加了存根类方法的模拟对象未释放,则存根方法将持续存在,即使在测试中也是如此。如果多个模拟对象同时操作同一类,则行为将不可预测。

4.10.2、期望Stub方法无效
id mock = OCMStrictClassMock([SomeClass class]);
OCMStub([mock someMethod]).andReturn(@"a string");
OCMExpect([mock someMethod]);

由于当前实现了模拟对象的方法,Stub会处理所有对它的调用。意味着即使调用了该方法,验证也会失败。避免此问题:

  • 方法1:通过andReturnExpect语句中添加
  • 方法2:在设置期望之后存根
4.10.3、不能为某些特殊类创建部分模拟
id partialMockForString = OCMPartialMock(@"Foo"); // 会抛出异常

NSDate *date = [NSDate dateWithTimeIntervalSince1970:0];
id partialMockForDate = OCMPartialMock(date); // 会对一些架构造成影响吗

无法为 toll-free bridged 类的实例创建局部模拟
无法为 某些实例创建以标记指针表示的对象,如:NSString、在某些体系结构上、NSDate在某些体系结构上

4.10.4、某些方法无法存根或验证
id partialMockForString = OCMPartialMock(anObject);
OCMStub([partialMock class]).andReturn(someOtherClass); // will not work

无法模拟许多核心运行时方法。包括:initclassmethodSignatureForSelector:forwardInvocation:respondsToSelector等等

4.10.5、NSString和NSArray上的类方法无法存根或验证
// 无法生效、该方法将不会被存根
id stringMock = OCMClassMock([NSString class]);
// 无法在NSString和NSArray上存根或验证类方法。尝试这样做没有任何效果。
OCMStub([stringMock stringWithContentsOfFile:[OCMArg any] encoding:NSUTF8StringEncoding error:[OCMArg setTo:nil]]);
4.10.6、NSManagedObject的类方法及其子类无法存根或验证
// 无法生效、该方法将不会被存根
id mock = OCMClassMock([MyManagedObject class]);
// 无法在其NSManagedObject或其子类上存根或验证类方法。尝试这样做没有任何效果。
OCMStub([mock someClassMethod]).andReturn(nil);
4.10.7、无法验证 NSObject 上的方法
id mock = OCMClassMock([NSObject class]);
/* run code under test, which calls awakeAfterUsingCoder: */
OCMVerify([mock awakeAfterUsingCoder:[OCMArg any]]); // still fails

不可能使用在 NSObject 中实现的方法或其上的类别进行运行后验证。
在某些情况下,可以对方法进行存根,然后对其进行验证。
当方法在子类中被覆盖时,可以使用运行后验证

4.10.8、无法验证核心 Apple 类中的私有方法
UIWindow *window = /* get window somehow */
id mock = OCMPartialMock(window);
/* run code under test, which causes _sendTouchesForEvent: to be invoked */
OCMVerify([mock _sendTouchesForEvent:[OCMArg any]]); // still fails

不可能在核心 Apple 类中使用私有方法运行后验证。
具体来说,在以 NSUI 作为前缀的类中,所有带有下划线前缀和/或后缀的方法。
在某些情况下,可以对方法进行存根,然后对其进行验证

4.10.9、运行后验证不能使用延迟

目前无法验证具有延迟的方法。这目前只能使用下面在严格模拟和期望中描述的expect-run-verify方法。

4.10.10、测试中使用多线程

OCMock 不是完全线程安全的。直到 4.2.x 版本 OCMock 根本不知道线程。来自多个线程的模拟对象上的任何操作组合都可能导致问题并使测试失败。

OCMock 4.3 开始,仍然需要从单个线程调用所有设置和验证操作,最好是测试运行程序的主线程。
但是,可以从多个线程使用模拟对象。模拟对象甚至可以在不同的线程中使用,而其设置在主线程中继续进行。

五、部分补充

5.1、单例的mock

不能直接mock单例的,会引起mock冲突。推荐的写法:

// 每次mock alloc 一个单例
id center = OCMPartialMock([[QLLoginCenter alloc] init]); 
// mock 它的 sharedInstance 方法
OCMStub([[center classMethod] sharedInstance]).andReturn(center); 



参考链接:
iOS中的测试:OCMock:https://www.jianshu.com/p/44ea034ac755
iOS_单元测试三之OCMock使用:https://blog.csdn.net/Margaret_MO/article/details/115420007
iOS_单元测试三之OCMockDemo:https://blog.csdn.net/Margaret_MO/article/details/118341525
iOS 单元测试之常用框架 OCMock 详解:https://www.51cto.com/article/707544.html
iOS单元测试-06-OCMoke和Stub详解:https://www.jianshu.com/p/6fd98f95d1ba

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容