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

[TOC]

一、Keep in mind:F.I.R.S.T

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

  • 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 官网上的文档 IntroductionDocumentationTutorials。其中 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 确定测试用例三要素

选定实现ControllerupdateTweetView方法,该方法通过调用connection成员的fetchTweets获取 Twitter 数据,然后调用tweetView成员的addTweet:将数据显示到界面。TDD 是测试先行,因此先编写针对updateTweetView方法的单元测试。

在此之前,需要考虑如何处理ControllerViewConnection的依赖。试想,如果选择直接构建ViewConnection的实例,则开发者会面临以下问题(结合 F.I.R.S.T 原则考虑),主要来自于Connection

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

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

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

因此,在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 数据显示到界面;

3.1.3 编写测试用例

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

- (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。具体代码如下:

- (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),触发了tweetViewaddTweet:方法显示 Tweet 数据到界面。

随便聊聊(可忽略):TDD(Test Driven Developing)是以上步骤不断迭代,以单元测试先行为核心原则,进行项目开发的过程。个人感觉,实际实施起来还是蛮有难度的,因为需要以非常清晰的逻辑思路、比较完备的前期模块设计准备、以及具备较高的确定性的需求为前提。

3.2 Mock的适用范围

并不是所有依赖都需要 mock,只要是不违背 F.I.R.S.T 原则的依赖,就可以直接构建依赖的实例。Test Smell: Everything is mocked中介绍了以下两种情况没有必要使用 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 类的实例

id classMock = OCMClassMock([SomeClass class]);

2、Mock 协议,即模拟构建一个遵循目标协议的 mock 对象

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

3、Mock strict 类和协议。在 strict 模式下的 mock 对象,若 when 元素触发了 strict mock 对象的未检验(verify)的方法,则会抛出异常,默认模式下则返回nil或默认返回类型。

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

4、Partial mock 是指 mock 对象和真实的实例具有相同的特性。Partial mock 对象接收到未 stub 的方法时,会转发给一个真实的实例anObject。注意:使用anObject触发已 stub 抑或是 未 stub 的方法,都可以通过 pratial mock 对该方法进行检验操作(verify)

id partialMock = OCMPartialMock(anObject);

5、构建可以观察 notification 的 mock 对象,注意必须先 mock 对象才能接收 notification

id observerMock = OCMObserverMock();

3.3.2 方法打桩(Stubbing)

1、指定 mock 对象响应 指定方法时,返回固定对象或值

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

2、指定 mock 对象响应 指定方法时,触发消息发送(target + selector),或者触发 Block(NSInvocation target + imp + signature + arguments)

OCMStub([mock someMethod]).andCall(anotherObject, @selector(aDifferentMethod));
OCMStub([mock someMethod]).andDo(^(NSInvocation *invocation)
    { /* block that handles the method invocation */ });

3、指定 mock 对象响应 指定方法时,设置该方法的 按引用传递的参数 的值

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 对象响应 指定方法时,抛出异常

OCMStub([mock someMethod]).andThrow(anException);

6、指定 mock 对象响应 指定方法时,发送通知

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

7、OCMStub对象支持链式编程语法

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

8、当使用 partial mock 以及 mock 类方法时,可以对一个方法打桩(stub)并且将该方法转发到真实对象(partial mock)或者类(class mock)响应。仅在使用链式语法或 strict mock 使用 expect 时才会用到

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

9、指定 mock 对象响应 指定方法时,什么也不做

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

3.3.3 交互检验(Verifying)

交互检验的 API 只有OCMVerify,用于检验测试过程中,某个方法是否有被调用,若没有调用,则会抛出单元测试失败的错误。注意,对 mock 对象的某个方法打桩,OCMVerify也会标记为该方法已被调用。

OCMVerify([mock someMethod]);

3.3.4 参数约束

参数约束就是限制目标 stub 方法的参数类型,参数类型不符则抛出测试失败错误。

1、参数可以为任何类型

OCMStub([mock someMethodWithAnArgument:[OCMArg any]])
OCMStub([mock someMethodWithPointerArgument:[OCMArg anyPointer]])
OCMStub([mock someMethodWithSelectorArgument:[OCMArg anySelector]])

2、指定 mock 忽略 invocation 中所有非对象类型的参数

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

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 */ }]])

4、使用 OCHamcrest 匹配模式

OCMStub([mock someMethod:startsWith(@"foo")])

3.3.5 Mock类方法

1、Stub 类方法。给类方法打桩的语法和给实例方法打桩的语法一模一样。然而给类方法打桩时,会修改所 mock 的类。因此给 mock 对象的类方法打桩,只要 mock 对象没有析构,则该类方法的操作会一直存在于整个测试中,若多个 mock 对象在同一个单元测试中操作相同的类,则会有操作彼此覆盖的风险

id classMock = OCMClassMock([SomeClass class]);
OCMStub([classMock aClassMethod]).andReturn(@"Test string");

// result is @"Test string"
NSString *result = [SomeClass aClassMethod];

2、检验 Stub 类方法的语法和实例方法一模一样

id classMock = OCMClassMock([SomeClass class]);
OCMStub([classMock aClassMethod]).andReturn(@"Test string");

/* run code under test */

OCMVerify([classMock aClassMethod]);

3、区分实例方法和类方法的 stub。当所要 stub 的 mock 对象的方法既是类方法也是实例方法时(实例方法和类方法同名)。则需要显式地指定 stub 的目标方法是否为类方法,使用ClassMethod(...)函数指定

id classMock = OCMClassMock([SomeClass class]);
OCMStub(ClassMethod([classMock ambiguousMethod])).andReturn(@"Test string");

// result is @"Test string"
NSString *result = [SomeClass ambiguousMethod];

4、Mock 类方法会改变类的方法列表结构,可以调用stopMocking恢复类到原始状态;

id classMock = OCMClassMock([SomeClass class]);

/* do stuff */

[classMock stopMocking];

3.3.6 Partial mocks:局部mock

1、Stub partial mock 对象的方法时,即使是向真实对象发送消息,也会作用于 partial mock 对象的 stub 以及 verify/expect。

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将真实对象恢复到原始状态。

[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(...)函数

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)

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

3、Expect 和 stub 一样支持andReturn()andThrow()等语法。

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、可以设置延迟一定时间后再进行检验

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 的顺序

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 可以模拟观察者,所观察的通知有被触发,检验才能通过

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的反面

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: 原文这句话好像缺了什么东西不太确定)

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可以模拟这个过程。

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

执行以上两行代码后,当向anObject发送someMethod消息时,anObjectsomeMethod不会被触发,而是触发differentObjectdifferentMethod方法。其他同类型的实例不会受上述过程的影响。someMethoddifferentMethod的方法名可以不相同,但是两者的签名(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: 这句没懂)

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 这些类型,测试会抛出异常。

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:

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

5、不可以 stub 或 verify NSStringNSArray的类方法

id stringMock = OCMClassMock([NSString class]);
// the following will not work

OCMStub([stringMock stringWithContentsOfFile:[OCMArg any] encoding:NSUTF8StringEncoding error:[OCMArg setTo:nil]]);

6、不可以 verify NSObject的方法。

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。

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 在 NSURLSessionNSURLConnection的请求发起阶段添加 hooker 函数实现模拟 HTTP 数据快速响应。由于 method swizzling 是一种比较霸道的扩展方式,因此必须在单元测试的tearDown中关闭 OHHTTPStub。OHHTTPStub 将 HTTP 模拟响应数据保存在文件系统中,操作起来更加方便。

4.4 异步测试

OCMock 中包含 delay expect 的 API,但是显然不好用,需要具体指定异步等待的时间。XCTest 框架中也包含测试异步过程的XCTestExpectation对象,当XCTestExpectation对象调用fulfill时,表示该 expectation 对应的异步过程执行完成,XCTTestCase类的waitForExpectationsWithTimeout系列接口用于检验。

- (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, 参数捕获和异步测试

[1] Testing iPhone View Controllers
[2] 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对象必然导致崩溃,因此为避免崩溃我们又要开始。

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