iOS单元测试-06-OCMoke和Stub详解

[TOC] ## 一、Keep in mind:F.I.R.S.T F.I.R.S.T 原则(参考[优秀测试实践原则](https://pragprog.com/magazines/2012-01/unit-tests-are-first)): - `Fast` — 测试应该能够被经常执行; - `Isolated` — 测试本身不能依赖于外部因素或其他测试的结果; - `Repeatable` — 每次运行测试都应该产生相同的结果; - `Self-verifying` — 测试应该依赖于断言,不需要人为干预; - `Timely` — 测试应该和生产代码一同书写; ## 二、单元测试基础 默认选项下,新建 Xcode 工程时会自动生成三个 target。Target 包含了编译器编译二进制文件(framework、.a、tbd、app、bundle等等)所需要知晓的所有信息,例如:编译的目标源文件、包含的资源文件、使用什么编译选项、指定支持架构类型等等,其实就是 XCode 里面 Build Setting、Build Phase 等几个选项卡里面的那一堆数据: - 编译工程输出二进制文件所使用的 target; - 单元测试 target(`Unit Testing Bundle`); - 自动化测试 target(`UI Testing Bundle`); >随便聊聊(**可忽略**):目前咱们的工程通常是没有构建单元测试 target 的,需要在`TARGETS`目录框下方的小加号添加`Unit Testing Bundle`类型的 target 用于添加单元测试。所有单元测试用例均添加到该 target 下。 ### 2.1 命名规范 Xcode 自动识别`XCTest`中的测试用例,其中`test`为前缀的方法为有效测试用例,更具体的有两种方式: 1. 统一`testThatIs`为尽量看齐 BDD 的命名方式,用测试用例名称描述测试用例的含义;2. 统一使用`test+{$方法名}`为更加传统的、面向代码的、稍微偏向于 `TDD` 的方式,描述测试用例所测试的目标方法。`DISABLED_`为前缀的方法为被禁止的测试用例。运行单元测试时,所有有效测试用例均会被执行,被禁止的测试用例则不执行。 有效测试用例: ``` - (void)testThatItDoesURLEncoding { /* test code */ } ``` 被禁止测试用例: ``` - (void)DISABLED_testThatItDoesURLEncoding { /* disabled test code */ } ``` >随便聊聊(**可忽略**):实际开发推荐使用 `BDD` 风格命名,因为通过浏览所有测试用例名称,就可以大概知道测试用例所实现的主体功能。咱们面向完成代码测试覆盖率指标则推荐使用 `TDD` 风格会更加直观,直接一个接口一个用例简单粗暴,况且方法名只要命名合理本身就自带功能描述性。 ### 2.2 测试用例的模式 测试用例都会包含三个因素: - `given`:通过创建模型对象或将被测试的系统设置到指定的状态,来设定测试环境,通常是设置目标测试接口的必要参数和上下文环境; - `when`:执行测试目标代码,通常是调用目标测试接口; - `then`:检查执行测试目标代码后,是否得到了我们期望的结果,通常是使用`XCAssertXXX`系列断言语句判断测试接口的输出结果是否满足预期; ### 2.3 setUp和tearDown 默认生成的`XCTestCase`测试用例,还会包含`-setUp()`和`-tearDown()`两个实例方法。 - `setUp`:用于初始化`XCTestCase`所有测试用例运行时 均需要具备的通用环境,- `tearDown`:则是用于`XCTestCase`所有测试用例执行完毕后,释放`setUp`所申请的资源。例如,`setUp`中建立了数据库连接,`tearUp`中就要释放数据库连接。 `XCTestCase`中的方法是有严格的执行顺序的: --> `setUp` ----> `testXXX`测试用例 ------> `tearDown`。 ## 三、Mock和Stub 关于 mock 和 stub 的理解,感觉网上很多文章 理解或表述得不太准确(本文应该也是不咋地),建议直接看 OCMock 官网上的文档 [Introduction](http://ocmock.org/introduction/)、[Documentation](http://ocmock.org/reference/)、[Tutorials](http://ocmock.org/support/)。其中 Totorial 基本涵盖了 mock 使用中常见的疑问和误区。 ### 3.1 Mock的存在价值 关于为什么需要 mock,OCMock 官网的 Introduction 举了以下一个例子(是个标准的 TDD 开发流程,值得学习一下):开发者需要开发一个从 Twitter 上拉取数据,然后更新用户界面的模块,如何应用 TDD 编写该模块的单元测试。接下来的内容,是根据 TDD 流程划分小节,关于 mock 的存在价值则分散在每个小节各处。 #### 3.1.1 模块划分 首先,划分大致模块,例如最简单的 MVC 模块划分方式,以确定接口。 **Controller** ``` /** Controller */ @interface Controller @property(retain) TwitterConnection *connection; @property(retain) TweetView *tweetView; - (void)updateTweetView; @end ``` **Data Source** ``` /** Data Source */ @interface TwitterConnection - (NSArray *)fetchTweets; @end ``` **View** ``` /** View */ @interface TweetView - (void)addTweet:(Tweet *)aTweet; @end ``` #### 3.1.2 确定测试用例三要素 选定**实现`Controller`的`updateTweetView`方法**,该方法通过调用`connection`成员的`fetchTweets`获取 Twitter 数据,然后调用`tweetView`成员的`addTweet:`将数据显示到界面。TDD 是测试先行,因此先编写针对`updateTweetView`方法的单元测试。 在此之前,需要考虑如何处理`Controller`对`View`和`Connection`的依赖。试想,如果选择直接构建`View`和`Connection`的实例,则开发者会面临以下问题(结合 F.I.R.S.T 原则考虑),主要来自于`Connection`: - 使用真实的网络连接必然大大增加单元测试的运行时长,会违背 Fast 原则; - Twitter可能在任何时间点返回任何数据,这样会面临两种都很差的选择: - 1、在单个单元测试中处理各种响应情况,这样会使单元测试逻辑流程依赖于 Twitter 的具体响应数据,违背了 Isolated 原则; - 2、针对不同的响应数据编写不同的测试用例,但这样不能保证所有用例的断言都被执行到,而且不同的响应会执行到不同的断言,这样违背了 Repeatly 原则; - Twitter一般不会返回错误,如 404、500,而且也很难控制 Twitter 返回特定的错误,同时也违背了 Self-verifying 原则; 因此,在`updateTweetView`单元测试中直接构建所依赖的`View`和`Connection`的实例是非常不明智的选择。于是 mock 便应运而生。`Mock 是用于在模块的单元测试中,模拟 模块所依赖的对象的特定行为或特定数据的 替身。`例如:可以指定 mock 对象的方法返回固定的`目标数据(stubbing)`、可以校验 mock 对象的方法是否有被触发(`verifying`)等等。Mock 可以使依赖的行为具备`可确定、可编辑、可追踪特性`。 回到刚才的例子,由于不需要等待网络数据同步返回,而是直接由 mock 返回模拟数据,因此符合 Fast 原则;另外返回模拟数据高度可控,使之符合 Isolated、Repeatly、Self-verifying 原则。 既然有这么优秀的选择,那就可以正式着手编写测试用例了。接下来编写测试用例:`Connection`从 Twitter 拉取数据成功后,若`Controller`调用`updateTweetView`,`View`是否有刷新数据。首先需要明确单元测试用例的三个基本因素: - `given`:`Connection`的`fetchTweet`方法指定能返回 Twitter 数据; - `when`:`Controller`实例调用了`updateTweetView`; - `then`:`View`是否有调用`addTweet`方法将 Twitter 数据显示到界面; #### 3.1.3 编写测试用例 由于测试的目标模块是`Controller`因此需要构建真实的实例,而依赖`Connection`和`TweetView`则只需构建其 mock 替身,并为`Controller`所持有,此时`Controller`是不知道它们只是 mock 对象。由于 mock `Connection` 的 `fetchTweets`操作的时间、数据不可确定性,所以需要给 `fetchTweets` 打桩(stub)返回固定的 Twitter 数据。当`Controller`实例调用`updateTweetView`方法时,需要验证(verify)mock `TweetView`的`addTweets:`显示 Twitter 数据到界面的操作被触发。 ```objc - (void)testDisplaysTweetsRetrievedFromConnection { //--------- Given Start ---------// // 1. 构建Controller实例 Controller *controller = [[[Controller alloc] init] autorelease]; // 2. Mock一个Connection实例 id mockConnection = OCMClassMock([TwitterConnection class]); controller.connection = mockConnection; // 3. stub Connection 的 fetchTweets 方法使之固定返回Tweet模型数组 Tweet *testTweet = /* create a tweet somehow */; NSArray *tweetArray = [NSArray arrayWithObject:testTweet]; OCMStub([mockConnection fetchTweets]).andReturn(tweetArray); // 4. Mock一个TweetView实例 id mockView = OCMClassMock([TweetView class]); controller.tweetView = mockView; //--------- When Start ---------// // 5. 调用测试目标方法updateTweetView [controller updateTweetView]; //--------- Then Start ---------// // 6. 验证 mock TweetView 的 addTweet: 显示Tweet到界面的操作被触发 OCMVerify([mockView addTweet:[OCMArg any]]); } ``` >注意:上述的模型`Tweet`是直接构建的,实际上模块中的有些依赖是不应该被 mock 的,例如:我们从来不会考虑去 mock `NSFoundation` 框架中定义的类。具体原因在 *3.2* 详细介绍。 #### 3.1.4 编写实现代码 完成了`updateTweetView`方法的测试用例,就可以大致清楚`updateTweetView`需要处理什么数据(stub)、需要调用依赖的哪些方法(verify)。此时运行该测试用例必然不通过,因为还未实现`updateTweetView`。接下来开始实现`updateTweetView`。具体代码如下: ```objc - (void)updateTweetView { NSArray *tweets = [connection fetchTweets]; if (tweets != nil) { for (Tweet t in tweets) [tweetView addTweet:t]; } else { /* handle error cases */ } } ``` 此时运行测试用例,用例通过,因为满足了测试用例中的`OCMVerify`的条件:当(given)`connection`固定正常返回 Tweet 数据时,调用`updateTweetView`时(when),触发了`tweetView`的`addTweet:`方法显示 Tweet 数据到界面。 >随便聊聊(**可忽略**):`TDD(Test Driven Developing)`是以上步骤不断迭代,以单元测试先行为核心原则,进行项目开发的过程。个人感觉,实际实施起来还是蛮有难度的,因为需要以非常清晰的逻辑思路、比较完备的前期模块设计准备、以及具备较高的确定性的需求为前提。 ### 3.2 Mock的适用范围 并不是所有依赖都需要 mock,只要是不违背 F.I.R.S.T 原则的依赖,就可以直接构建依赖的实例。[Test Smell: Everything is mocked](http://www.mockobjects.com/2007/04/test-smell-everything-is-mocked.html)中介绍了以下两种情况没有必要使用 mock: - `值类型的对象`。判断是否为值类型对象的原则有二:1、仅包含属性及访问器的、或者只是简单的操作其持有的数据、或者没包含任何需要注意的特定行为的对象;2、压根就没有什么复杂行为的类,例如你无论如何也不会想到给这个类取名为`XXXImpl`的类(注意:这一点不知道有没理解准确); - `第三方库`。原因有二:1、`第三方库的实现细节不可知`,其处理模式未必是合理的,没有必要为了适应第三方库潜在的设计不合理性而增加项目自身的单元测试的复杂度;2、`有 mock 必然有 verify mock`,因此如果 mock 第三方库则必然会在单元测试中引入第三方库的实现细节,而且还必须得保证单元测试的这些逻辑必须符合第三方库实际的实现细节,而这些细节对第三方库的使用者而言是没有必要知晓的。 对于值类型对象,在单元测试中只要直接构建其实例即可,没必要引入 mock 徒增单元测试的复杂度。对于第三方库的测试,根据上面参考文章的观点:可以根据项目所要用到的第三方库的功能编写协议,并编写一层很薄的中间层使用第三方库的接口实现该协议,调用中间层的接口做集成测试,由于集成测试的数量较少所以第三方库的接口调用的耗时问题也不会对整个单元测试有太大的影响。 个人则不太赞同这个观点,对于第三方库接口调用的时间、返回数据存在不可控性的第三方库,是需要 mock 的,但是重点应该放在 stub 上,而 mock verify 的操作则完全没有必要的,因为不需要知道第三方库的内部实现细节。我们的目的在于保证第三方库能够快速返回 其公开接口 API 所约定的、确定性的、而且合法的返回数据。 ### 3.3 OCMock基本API #### 3.3.1 构建mock对象 1、Mock 类的实例 ```objc id classMock = OCMClassMock([SomeClass class]); ``` 2、Mock 协议,即模拟构建一个遵循目标协议的 mock 对象 ```objc id protocolMock = OCMProtocolMock(@protocol(SomeProtocol)); ``` 3、Mock strict 类和协议。在 strict 模式下的 mock 对象,若 when 元素触发了 strict mock 对象的未检验(verify)的方法,则会抛出异常,默认模式下则返回`nil`或默认返回类型。 ```objc id classMock = OCMStrictClassMock([SomeClass class]); id protocolMock = OCMStrictProtocolMock(@protocol(SomeProtocol)); ``` 4、Partial mock 是指 mock 对象和真实的实例具有相同的特性。Partial mock 对象接收到未 stub 的方法时,会转发给一个真实的实例`anObject`。注意:使用`anObject`触发已 stub 抑或是 未 stub 的方法,都可以通过 pratial mock 对该方法进行检验操作(verify) ```objc id partialMock = OCMPartialMock(anObject); ``` 5、构建可以观察 notification 的 mock 对象,注意必须先 mock 对象才能接收 notification ```objc id observerMock = OCMObserverMock(); ``` #### 3.3.2 方法打桩(Stubbing) 1、指定 mock 对象响应 指定方法时,返回固定对象或值 ```objc OCMStub([mock someMethod]).andReturn(anObject); OCMStub([mock aMethodReturningABoolean]).andReturn(YES); ``` 2、指定 mock 对象响应 指定方法时,触发消息发送(target + selector),或者触发 Block(`NSInvocation` target + imp + signature + arguments) ```objc OCMStub([mock someMethod]).andCall(anotherObject, @selector(aDifferentMethod)); OCMStub([mock someMethod]).andDo(^(NSInvocation *invocation) { /* block that handles the method invocation */ }); ``` 3、指定 mock 对象响应 指定方法时,设置该方法的 按引用传递的参数 的值 ```objc OCMStub([mock someMethodWithReferenceArgument:[OCMArg setTo:anObject]]); OCMStub([mock someMethodWithReferenceArgument:[OCMArg setToValue:OCMOCK_VALUE((int){aValue})]]); ``` 4、指定 mock 对象响应 指定方法时,触发该方法的 Block 类型参数 ``` OCMStub([mock someMethodWithBlock:[OCMArg invokeBlock]]); OCMStub([mock someMethodWithBlock:([OCMArg invokeBlockWithArgs:@"First arg", nil])]); ``` 5、指定 mock 对象响应 指定方法时,抛出异常 ```objc OCMStub([mock someMethod]).andThrow(anException); ``` 6、指定 mock 对象响应 指定方法时,发送通知 ```objc OCMStub([mock someMethod]).andPost(aNotification); ``` 7、`OCMStub`对象支持链式编程语法 ```objc OCMStub([mock someMethod]).andPost(aNotification).andReturn(aValue); ``` 8、当使用 partial mock 以及 mock 类方法时,可以对一个方法打桩(stub)并且将该方法转发到真实对象(partial mock)或者类(class mock)响应。仅在使用链式语法或 strict mock 使用 expect 时才会用到 ```objc OCMStub([mock someMethod]).andForwardToRealObject(); ``` 9、指定 mock 对象响应 指定方法时,什么也不做 ```objc OCMStub([mock someMethod]).andDo(nil); ``` #### 3.3.3 交互检验(Verifying) 交互检验的 API 只有`OCMVerify`,用于检验测试过程中,某个方法是否有被调用,若没有调用,则会抛出单元测试失败的错误。注意,对 mock 对象的某个方法打桩,`OCMVerify`也会标记为该方法已被调用。 ```objc OCMVerify([mock someMethod]); ``` #### 3.3.4 参数约束 参数约束就是限制目标 stub 方法的参数类型,参数类型不符则抛出测试失败错误。 1、参数可以为任何类型 ```objc OCMStub([mock someMethodWithAnArgument:[OCMArg any]]) OCMStub([mock someMethodWithPointerArgument:[OCMArg anyPointer]]) OCMStub([mock someMethodWithSelectorArgument:[OCMArg anySelector]]) ``` 2、指定 mock 忽略 invocation 中所有非对象类型的参数 ```objc [[[mock stub] ignoringNonObjectArgs] someMethodWithIntArgument:0] ``` 3、指定更具体的参数约束 ```objc 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 */ }]]) ``` 4、使用 [OCHamcrest](http://hamcrest.org/OCHamcrest/) 匹配模式 ```objc OCMStub([mock someMethod:startsWith(@"foo")]) ``` #### 3.3.5 Mock类方法 1、Stub 类方法。给类方法打桩的语法和给实例方法打桩的语法一模一样。然而给类方法打桩时,会修改所 mock 的类。因此给 mock 对象的类方法打桩,只要 mock 对象没有析构,则该类方法的操作会一直存在于整个测试中,若多个 mock 对象在同一个单元测试中操作相同的类,则会有操作彼此覆盖的风险 ```objc id classMock = OCMClassMock([SomeClass class]); OCMStub([classMock aClassMethod]).andReturn(@"Test string"); // result is @"Test string" NSString *result = [SomeClass aClassMethod]; ``` 2、检验 Stub 类方法的语法和实例方法一模一样 ```objc id classMock = OCMClassMock([SomeClass class]); OCMStub([classMock aClassMethod]).andReturn(@"Test string"); /* run code under test */ OCMVerify([classMock aClassMethod]); ``` 3、区分实例方法和类方法的 stub。当所要 stub 的 mock 对象的方法既是类方法也是实例方法时(实例方法和类方法同名)。则需要显式地指定 stub 的目标方法是否为类方法,使用`ClassMethod(...)`函数指定 ```objc id classMock = OCMClassMock([SomeClass class]); OCMStub(ClassMethod([classMock ambiguousMethod])).andReturn(@"Test string"); // result is @"Test string" NSString *result = [SomeClass ambiguousMethod]; ``` 4、Mock 类方法会改变类的方法列表结构,可以调用`stopMocking`恢复类到原始状态; ```objc id classMock = OCMClassMock([SomeClass class]); /* do stuff */ [classMock stopMocking]; ``` #### 3.3.6 Partial mocks:局部mock 1、Stub partial mock 对象的方法时,即使是向真实对象发送消息,也会作用于 partial mock 对象的 stub 以及 verify/expect。 ```objc 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]; OCMVerify([partialMock someMethod]); ``` 2、由于 partial mock 会影响真实对象的方法响应结构,因此当不在需要 partial mock 对象时,需要调用`stopMocking`将真实对象恢复到原始状态。 ```objc [partialMock stopMocking]; ``` #### 3.3.7 Strict mocks and expectations:绝对mock和预期 1、Strict mock 是具有更高检验要求的 mock 对象,stub strict mock 使用 expect 而不是 verify。Strict mock 本质也是 mock,甚至可以构建一个普通 mock 对象,但是使用 expect 打桩以实现 strict mock 效果。检验 strict mock 使用`OCMVerifyAll(...)`函数 ```objc id classMock = OCMClassMock([SomeClass class]); OCMExpect([classMock someMethodWithArgument:[OCMArg isNotNil]]); /* run code under test, which is assumed to call someMethod */ OCMVerifyAll(classMock) ``` 2、若使用`OCMStrictClassMock(...)`构建一个 strict mock,但是没有指定任何 expect 操作,则单元测试会抛出异常(execption) ```objc id classMock = OCMStrictClassMock([SomeClass class]); [classMock someMethod]; // this will throw an exception ``` 3、Expect 和 stub 一样支持`andReturn()`、`andThrow()`等语法。 ```objc 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) ``` 4、可以设置延迟一定时间后再进行检验 ```objc id mock = OCMStrictClassMock([SomeClass class]); OCMExpect([mock someMethod]); /* run code under test, which is assumed to call someMethod eventually */ OCMVerifyAllWithDelay(mock, aDelay); ``` 5、Expect 是具有顺序的,指定的 stub 方法的触发过程必须按照 expect 的顺序 ```objc 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]; ``` #### 3.3.8 Observer mocks:观察者mock Observer mock 可以模拟观察者,所观察的通知有被触发,检验才能通过 ```objc id observerMock = OCMObserverMock(); [notificatonCenter addMockObserver:aMock name:SomeNotification object:nil]; [[mock expect] notificationWithName:SomeNotification object:[OCMArg any]]; OCMVerifyAll(observerMock); ``` > 注意:上面的使用 expect 语法是用了 OCMock 的旧语法风格,一般使用链式函数式的新语法风格。选择用旧语法的准则是:当对象没有该 mock 的响应的时候,就是用旧语法,例如`notificationWithName`明显不是 mock 对象的方法。 #### 3.3.9 进阶 **1、该选择 nice 还是 strict mock?** 当 verify strict mock 之前,没有对 strict mock 指定任何 expect 条件(stub),会抛出异常(failing fast),但是对于普通的 mock(nice)则会返回默认值。使用`OCMReject`指定禁止触发某方法,若方法触发则测试失败,`OCMReject`其实就是`OCMVerify`的反面 ```objc id mock = OCMClassMock([SomeClass class]); OCMReject([mock someMethod]); ``` **2、Fail fast 异常有时不会导致测试立即失败。** 这种情况在单元测试中,方法的调用栈未执行完毕时可能会出现。Fail fast 异常会在`OCMVerifyAll`调用时抛出,这保证了通知中非必须的 invocation 等可以被检测到(TODO:这里不太懂)。 >原文:In fail-fast mode an exception might not cause the test to fail. This can happen when the call stack for the method does not end in the test. Fail fast exceptions will be re-thrown when OCMVerifyAll is called. This makes it possible to ensure that unwanted invocations from notifications etc. can be detected. **3、允许 stub 类中用于构建对象的方法,如`copy`。** 可以 `stub new` 方法,但如果大面积使用则推荐使用依赖注入模式(`dependency injection`)。不可以 `stub init` 方法,因为 mock 对象的基本类型本身实现了`init`方法。但是当 mock 对象完成初始化后简单地返回`self`后,会再一次调用`init`方法(TODO: 原文这句话好像缺了什么东西不太确定) ```objc id classMock = OCMClassMock([SomeClass class]); OCMStub([classMock copy])).andReturn(myObject); id classMock = OCMClassMock([SomeClass class]); OCMStub([classMock new])).andReturn(myObject); ``` **4、`Method swizzling` 是指在运行时用指定的`IMP`交换指定方法的原有`IMP`。** 使用 partial mock 的`andCall`可以模拟这个过程。 ```objc id partialMock = OCMPartialMock(anObject); OCMStub([partialMock someMethod]).andCall(differentObject, @selector(differentMethod)); ``` 执行以上两行代码后,当向`anObject`发送`someMethod`消息时,`anObject`的`someMethod`不会被触发,而是触发`differentObject`的`differentMethod`方法。其他同类型的实例不会受上述过程的影响。`someMethod`和`differentMethod`的方法名可以不相同,但是两者的签名(signature)必须一致。 #### 3.3.10 使用场景限制 **1、Stub 类方法时,不能同时 stub 同一个类的类方法。** 因为 stub 类方法会改变类的元数据,同时 stub 同一个类的类方法会造成 mock 的类的元数据混乱。 ``` // don't do this id mock1 = OCMClassMock([SomeClass class]); OCMStub([mock1 aClassMethod]); id mock2 = OCMClassMock([SomeClass class]); OCMStub([mock2 anotherClassMethod]); ``` **2、Stub 某个方法然后 expect 检验该方法,expect 的结果是该方法未调用。** 这是因为调用`someMethod`实际上是由 stub 控制。通过给 expect 语句添加`andReturn`可以规避该问题,You can also set up a stub after the expect(TODO: 这句没懂) ```objc 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 ``` **3、Partial mock 不支持 toll-free bridged 类型** 例如:`NSString`、用 tagged pointer 表示的对象,例如一些结构下的`NSDate`。若强行 partial mock 这些类型,测试会抛出异常。 ```objc id partialMockForString = OCMPartialMock(@"Foo"); // will throw an exception NSDate *date = [NSDate dateWithTimeIntervalSince1970:0]; id partialMockForDate = OCMPartialMock(date); // will throw on some architectures ``` **4、有些方法不可以 stub。** 例如:`init`, `class`, `methodSignatureForSelector:`, `forwardInvocation:`, `respondsToSelector:`等 ```objc id partialMockForString = OCMPartialMock(anObject); OCMStub([partialMock class]).andReturn(someOtherClass); // will not work ``` **5、不可以 stub 或 verify `NSString`和`NSArray`的类方法** ```objc id stringMock = OCMClassMock([NSString class]); // the following will not work OCMStub([stringMock stringWithContentsOfFile:[OCMArg any] encoding:NSUTF8StringEncoding error:[OCMArg setTo:nil]]); ``` **6、不可以 verify `NSObject`的方法。** ```objc id mock = OCMClassMock([NSObject class]); /* run code under test, which calls awakeAfterUsingCoder: */ OCMVerify([mock awakeAfterUsingCoder:[OCMArg any]]); // still fails ``` **7、不可以 verify 苹果私有 API,尤其是下划线开头。但是可以 stub 私有 API 然后使用 verify。** ```objc 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 ``` **8、Verify 不可以使用延迟时间,但是 expect 可以。** **9、OCMock 并不是线程安全的** 多线程下操作同一个 mock 对象有可能会出现数据混乱,最终导致测试失败。 ## 四、常见问题 ### 4.1 Mock委托、Block、通知 其实三者都有很相似的地方,都是用于降低耦合度,也都经常出现业务代码中。 测试持有 delegate 的对象时,需要 `mock delegate` 对象,并在调用了会触发的 delegate 方法 的方法后,`verify delegate` 方法成功触发;测试遵循 delegate 协议的对象时,则直接 `verify delegate` 方法中需要完成的操作细节即可。 Block 可以实现委托,不过 block 实现委托需要特别注意循环引用问题。`测试持有 block 的对象时,需要在调用了会触发 block 的方法后,verify block 成功被触发`;测试定义 block 的对象时,则使用前面介绍的`andDo`语法通过 stub 触发 block,并 verify block 主体内需要完成的操作细节即可。 通知的 mock 和 stub 则直接参照 *3.3.8* 中的介绍。注意:observer mock 必定为 strict mock,使用 expect 检验,且 observer mock 必须注册才能接收通知。 ### 4.2 Mock数据库 数据库连接和释放需要消耗不少的运行时间,而且也要保证返回数据的可控性,因此通常会 `mock 数据实例`,并使用 stub 返回想要的测试数据。 ### 4.3 Stub网络请求:OHHTTPStub 对于 HTTP 数据响应的模拟,Github 上开源的第三方库 OHHTTPStub 的网络请求模拟机制以及提供的 API 更具有针对性。OHHTTPStub 通过 method swizzling 在 `NSURLSession`和`NSURLConnection`的请求发起阶段添加 hooker 函数实现模拟 HTTP 数据快速响应。由于 method swizzling 是一种比较霸道的扩展方式,因此必须在单元测试的`tearDown`中关闭 OHHTTPStub。OHHTTPStub 将 HTTP 模拟响应数据保存在文件系统中,操作起来更加方便。 ### 4.4 异步测试 OCMock 中包含 delay expect 的 API,但是显然不好用,需要具体指定异步等待的时间。XCTest 框架中也包含测试异步过程的`XCTestExpectation`对象,当`XCTestExpectation`对象调用`fulfill`时,表示该 expectation 对应的异步过程执行完成,`XCTTestCase`类的`waitForExpectationsWithTimeout`系列接口用于检验。 ```objc - (void)testExpectation{ XCTestExpectation* expect = [self expectationWithDescription:@"Oh, timeout!"]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ sleep(2); //延迟两秒向下执行 XCTAssert(YES,"Some Error Info");//通过测试 [expect fulfill];//告知异步测试结束 }); [self waitForExpectationsWithTimeout:10 handler:^(NSError *error) { //等待10秒,若该测试未结束(未收到 fulfill方法)则测试结果为失败 //Do something when time out }]; } ``` ### 4.5 测试Controller Controller 是 MVC 模式下最复杂的模块,由于作为中介者必然涉及了多方通信从而导致其依赖关系复杂,Controller 甚至被认为不适合被测试。测试 Controller 参考上善若水的博客 [Kiwi 使用进阶 Mock, Stub, 参数捕获和异步测试](https://onevcat.com/2014/05/kiwi-mock-stub-test/) [1] [Testing iPhone View Controllers](https://blog.carbonfive.com/2010/03/10/testing-view-controllers/) [2] [Testing Cocoa Controllers with OCMock](https://erik.doernenburg.com/2008/07/testing-cocoa-controllers-with-ocmock/) ## 六、项目实践 SVProgressHUD存在同名的dismiss的实例方法和类方法(TODO) **如何测试Controller** Controller作为中介者很容易陷如逻辑过于臃肿的泥潭,如何测试Controller呢。 1、首先XIB载入问题,如果要测试XIB中所有IBOutlet、IBAction是否正常绑定,则需要从XIB读取所有真实数据,不需要Mock界面上的任何元素; 2、然后是viewDidLoad等初始化阶段的测试,在这个阶段,Controller已经与依赖模块有一定的交互动作,为了保证单元测试的独立性,理论上Controller所有依赖都要进行Mocking处理。但是鉴于OCMock的使用场景限制,以及单元测试编写效率方面考虑,简单的Model、NSFoundation中的类一般不需要Mock。而**项目中定义的较复杂的模块必须Mock**,这样又会导致一些问题,例如我们Mock了一个继承UIView类型的自定义模块,若代码中存在addSubview添加该自定义模块为子视图,则传如Mock对象必然导致崩溃,因此为避免崩溃我们又要开始。

你可能感兴趣的:(iOS单元测试-06-OCMoke和Stub详解)