iOS单元测试

一、什么是单元测试

在计算机编程中,单元测试(英语:Unit Testing)又称为模块测试, 是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。

通常来说,程序员每修改一次程序就会进行最少一次单元测试,在编写程序的过程中前后很可能要进行多次单元测试,以证实程序达到软件规格书要求的工作目标,没有程序错误;虽然单元测试不是什么必须的,但也不坏,这牵涉到项目管理的政策决定。

每个理想的测试案例独立于其它案例;为测试时隔离模块,经常使用stubs、mock或fake等测试马甲程序。单元测试通常由软件开发人员编写,用于确保他们所写的代码匹配软件需求和遵循开发目标。它的实施方式可以是非常手动的(通过纸笔),或者是做成构建自动化的一部分。


一、为什么需要单元测试

1、利

  • 避免低级错误,保证代码的质量

疏忽造成的拼写错误,注意力不足造成调用的错误,对 API 望文生义的理解造成的方法调用错误等。根据墨菲定律,如果事情有变坏的可能,不管这种可能性有多小,它总会发生。而单元测试可以作为一个很好的补充。

代码可以通过编译器检查语法的正确性,却不能保证代码逻辑是正确的,尤其包含了许多单元分支的情况下,单元测试可以保证代码的行为和结果与我们的预期和需求一致。

  • 及早发现问题

问题、压力、风险提前释放到开发阶段。
这里写图片描述

  • 减少调试时间

在缺少宿主程序的情况下,单元测试可以充当宿主程序。而即使存在宿主程序,单元测试相对于宿主程序也有着入口简单,方便执行的优点,不再需要通过复杂的流程才能够进行对应方法的测试,大大减少调试时间。

  • 增加可维护性、可扩展性

这是我最推崇单元测试的原因,随着代码量膨胀,代码和代码之间的边界越来越模糊,新代码的加入对旧代码的影响并不是都能够通过逻辑推导获知。但对一个模块添加足够的单元测试后,新代码的加入可以在第一时间内测试完毕对旧逻辑的影响,增加整个模块的可维护性,减少这部分的测试工作,将更多的时间投入到更有意义的事情上去,如喜闻乐见的重构。

为了保证可行的可持续的单元测试,程序单元应该是低耦合的,否则,单元测试将难以进行。

  • 帮助改善设计

接上一条,重构是贯穿于项目工程的一件任务,随着时间推移,项目总归会慢慢产生各种技术债务,渐进式的重构是很好的还债手段。单元测试可以第一时间揭示重构带来的问题,让我们大胆地进行调整,改善既有设计,实现一个良好的循环。


2、弊

  • 不能减少研发的代码量,反而会花费很多精力在编写单元测试上,增加了开发成本,而且对开发人员的要求也会更高。

  • 对于小项目来说,是否执行单元测试意义不大。

  • 单元测试聚焦的是一个模块单元的功能完整性和健壮性,但是模块间互动可能带来的问题并不属于单元测试的范畴,同时也有很大部分的界面测试和功能测试仍旧离不开测试工程。


我们知道,代码的最终目标有两个:
(1)实现需求;
(2)提高代码质量和可维护性。
我认为单元测试时有必要的。


二、哪些需要单元测试

单元测试侧重的是逻辑测试和接口测试。实际操作过程中,要自下而上进行。从最基础的 Base 层,往上写测试。确保基础的 Model,Manager 测试通过,才开始为 Controller 编写测试,因为这部分业务是最复杂的,也是最容易改变的。

1、网络数据层;
2、公共类中的公开方法;
3、业务逻辑层;
4、修复Bug的测试。


三、怎么进行单元测试

1、单元测试准则

  • 保持单元测试小巧, 快速

理论上, 任何代码提交前都应该完整跑一遍所有测试套件. 保持测试代码执行迅捷能够缩短迭代开发周期。

  • 单元测试应该是全自动且无交互

测试套件通常是定期执行的, 执行过程必须完全自动化才有意义. 需要人工检查输出结果的测试不是一个好的单元测试。

  • 让单元测试很容易跑起来

对开发环境进行配置, 最好是敲条命令或是点个按钮就能把单个测试用例或测试套件跑起来。

  • 对测试进行评估

对执行的测试进行覆盖率分析, 得到精确的代码执行覆盖率, 并调查哪些代码未被执行。

  • 立即修正失败的测试

每个开发人员在提交前都应该保证新的测试用例执行成功, 当有代码提交时, 现有测试用例也都能跑通。

如果一个定期执行的测试用例执行失败, 整个团队应该放下手上的工作优先解决这个问题。

  • 把测试维持在单元级别

单元测试即类 (Class) 的测试。一个 “测试类” 应该只对应于一个 “被测类”,并且 “被测类” 的行为应该被隔离测试。必须谨慎避免使用单元测试框架来测试整个程序的工作流,这样的测试既低效又难维护。工作流测试 (译注: 指跨模块/类的数据流测试) 有它自己的地盘, 但它绝不是单元测试,必须单独建立和执行。

  • 由简入繁

最简单的测试也远远胜过完全没有测试. 一个简单的 “测试类” 会促使建立 “被测类” 基本的测试骨架, 可以对构建环境, 单元测试环境, 执行环境以及覆盖率分析工具等有效性进行检查, 同时也可以证明 “被测类” 能够被整合和调用.

下面便是单元测试版的 Hello, world! :

void testDefaultConstruction()
{
Foo foo = new Foo();
assertNotNull(foo);
}
  • 保持测试的独立性

为了保证测试稳定可靠且便于维护, 测试用例之间决不能有相互依赖, 也不能依赖执行的先后次序.

  • Keep tests close to the class being tested

本条规则值得商榷, 大部分 C++, Objective-C和 Python 库均把测试代码从功能代码目录中独立出来, 通常是创建一个和 src 目录同级的 tests 目录, 被测模块/类名之前也常常 不加 Test 前缀. 这么做保证功能代码和测试代码隔离, 目录结构清晰, 并且发布源码的时候更容易排除测试用例.

If the class to test is Foo the test class should be called FooTest (not TestFoo) and kept in the same package (directory) as Foo. Keeping test classes in separate directory trees makes them harder to access and maintain.
Make sure the build environment is configured so that the test classes doesn’t make its way into production libraries or executables.

  • 合理的命名测试用例

确保每个方法只测试 “被测类” 的一个明确特性, 并相应的命名测试方法. 典型的命名俗定是 test[what], 比如 testSaveAs(), testAddListener(), testDeleteProperty() 等.

  • 只测公有接口

单元测试可以被定义为 通过类的公有 API 对类进行测试. 一些测试工具允许测试一个类的私有成员, 但这种做法应该避免, 它让测试变得繁琐而且更难维护. 如果有私有成员确实需要进行直接测试, 可以考虑把它重构到工具类的公有方法中. 但要注意这么做是为了改善设计, 而不是帮助测试.

  • 看成是黑盒

站在第三方使用者的角度, 测试一个类是否满足规定的需求. 并设法让它出问题.

  • 看成是白盒

毕竟被测试类是程序员自写自测的, 应该在最复杂的逻辑部分多花些精力测试.

  • 芝麻函数也要测试

通常建议所有重要的函数都应该被测试到, 一些芝麻方法比如简单的 setter 和 getter 都可以忽略. 但是仍然有充分的理由支持测试芝麻函数:
<1>芝麻 很难定义. 对于不同的人有不同的理解.
<2>从黑盒测试的观点看, 是无法知道哪些代码是芝麻级别的.
<3>即便是再芝麻的函数, 也可能包含错误, 通常是 “复制粘贴” 代码的后果:

private double weight_;
private double x_, y_;

public void setWeight(int weight)
{
  weight = weight_;  // error
}

public double getX()
{
  return x_;
}

public double getY()
{
  return x_;  // error
}

因此建议测试所有方法. 毕竟芝麻用例也容易测试.

  • 先关注执行覆盖率

区别对待 执行覆盖率 和 实际测试覆盖率. 测试的最初目标应该是确保较高的执行覆盖率. 这样能保证代码在 少量 参数值输入时能执行成功. 一旦执行覆盖率就绪, 就应该开始改进测试覆盖率了. 注意, 实际的测试覆盖率很难衡量 (而且往往趋近于 0%).

思考以下公有方法:

void setLength(double length);

调用 setLength(1.0) 你可能会得到 100% 的执行覆盖率. 但要达到 100% 的实际测试覆盖率, 有多少个 double 浮点数这个方法就必须被调用多少次, 并且要一一验证行为的正确性. 这无疑是不可能的任务.

  • 覆盖边界值

确保参数边界值均被覆盖. 对于数字, 测试负数, 0, 正数, 最小值, 最大值, NaN (非数字), 无穷大等. 对于字符串, 测试空字符串, 单字符, 非 ASCII 字符串, 多字节字符串等. 对于集合类型, 测试空, 1, 第一个, 最后一个等. 对于日期, 测试 1月1号, 2月29号, 12月31号等. 被测试的类本身也会暗示一些特定情况下的边界值. 要点是尽可能彻底的测试这些边界值, 因为它们都是主要 “疑犯”.

  • 提供一个随机值生成器

当边界值都覆盖了, 另一个能进一步改善测试覆盖率的简单方法就是生成随机参数, 这样每次执行测试都会有不同的输入.

想要做到这点, 需要提供一个用来生成基本类型 (如: 浮点数, 整型, 字符串, 日期等) 随机值的工具类. 生成器应该覆盖各种类型的所有取值范围.

如果测试时间比较短, 可以考虑再裹上一层循环, 覆盖尽可能多的输入组合. 下面的例子是验证两次转换 little endian 和 big endian 字节序后是否返回原值. 由于测试过程很快, 可以让它跑上个一百万次.

void testByteSwapper()
{
  for (int i = 0; i < 1000000; i++) {
    double v0 = Random.getDouble();
    double v1 = ByteSwapper.swap(v0);
    double v2 = ByteSwapper.swap(v1);
    assertEquals(v0, v2);
  }
}
  • 每个特性只测一次

在测试模式下, 有时会情不自禁的滥用断言. 这种做法会导致维护更困难, 需要极力避免. 仅对测试方法名指示的特性进行明确测试.

因为对于一般性代码而言, 保证测试代码尽可能少是一个重要目标.

  • 使用显式断言

应该总是优先使用 assertEquals(a, b) 而不是 assertTrue(a == b), 因为前者会给出更有意义的测试失败信息. 在事先不确定输入值的情况下, 这条规则尤为重要, 比如之前使用随机参数值组合的例子.

  • 提供反向测试

反向测试是指刻意编写问题代码, 来验证鲁棒性和能否正确的处理错误.

假设如下方法的参数如果传进去的是负数, 会立马抛出异常:

void setLength(double length) throws IllegalArgumentExcepti

可以用下面的方法来测试这个特例是否被正确处理:

try {
  setLength(-1.0);
  fail();  // If we get here, something went wrong
}
catch (IllegalArgumentException exception) {
  // If we get here, all is fine
}
  • 代码设计时谨记测试

编写和维护单元测试的代价是很高的, 减少代码中的公有接口和循环复杂度是降低成本, 使高覆盖率测试代码更易于编写和维护的有效方法.

一些建议:

<1>使类成员常量化, 在构造函数中进行初始化. 减少 setter 方法的数量.

<2>限制过度使用继承和公有虚函数.
<3>通过使用友元类 (C++) 或包作用域 (Java) 来减少公有接口.
<4>避免不必要的逻辑分支.

<5>在逻辑分支中编写尽可能少的代码.

<6>在公有和私有接口中尽量多用异常和断言验证参数参数的有效性.

<7>限制使用快捷函数. 对于黑箱而言, 所有方法都必须一视同仁的进行测试. 思考以下简短的例子:

public void scale(double x0, double y0, double scaleFactor)
{
  // scaling logic
}

public void scale(double x0, double y0)
{
  scale(x0, y0, 1.0);
}

删除后者可以简化测试, 但用户代码的工作量也将略微增加.

  • 不要访问预设的外部资源

单元测试代码不应该假定外部的执行环境, 以便在任何时候/任何地方都能执行. 为了向测试提供必需的资源, 这些资源应该由测试本身提供.

比如一个解析某类型文件的类, 可以把文件内容嵌入到测试代码里, 在测试的时候写入到临时文件, 测试结束再删除, 而不是从预定的地址直接读取.

  • 权衡测试成本

不写单元测试的代价很高, 但是写单元测试的代价同样很高. 要在这两者之间做适当的权衡, 如果用执行覆盖率来衡量, 业界标准通常在 80% 左右.

很典型的, 读写外部资源的错误处理和异常处理就很难达到百分百的执行覆盖率. 模拟数据库在事务处理到一半时发生故障并不是办不到, 但相对于进行大范围的代码审查, 代价可能太大了.

  • 安排测试优先次序

单元测试是典型的自底向上过程, 如果没有足够的资源测试一个系统的所有模块, 就应该先把重点放在较底层的模块.

  • 测试代码要考虑错误处理

考虑下面的这个例子:

Handle handle = manager.getHandle();
assertNotNull(handle);

String handleName = handle.getName();
assertEquals(handleName, "handle-01");

如果第一个断言失败, 后续语句会导致代码崩溃, 剩下的测试都无法执行. 任何时候都要为测试失败做好准备, 避免单个失败的测试项中断整个测试套件的执行. 上面的例子可以重写成:

Handle handle = manager.getHandle();
assertNotNull(handle);
if (handle == null) return;

String handleName = handle.getName();
assertEquals(handleName, "handle-01");
  • 写测试用例重现 bug

每上报一个 bug, 都要写一个测试用例来重现这个 bug (即无法通过测试), 并用它作为成功修正代码的检验标准.

  • 了解局限

单元测试永远无法证明代码的正确性!!

一个跑失败的测试可能表明代码有错误, 但一个跑成功的测试什么也证明不了.

单元测试最有效的使用场合是在一个较低的层级验证并文档化需求, 以及 回归测试: 开发或重构代码时,不会破坏已有功能的正确性.


2、思想

测试驱动开发(Test-driven development,TDD)是一种软件开发过程中的应用方法,由极限编程中倡导,以其倡导先写测试程序,然后编码实现其功能得名。测试驱动开发是戴两顶帽子思考的开发方式:先戴上实现功能的帽子,在测试的辅助下,快速实现其功能;再戴上重构的帽子,在测试的保护下,通过去除冗余的代码,提高代码质量。测试驱动着整个开发过程:首先,驱动代码的设计和功能的实现;其后,驱动代码的再设计和重构。
  
行为驱动开发(Behavior-driven development,BDD)是一种敏捷软件开发的技术,BDD的重点是通过与利益相关者的讨论取得对预期的软件行为的清醒认识。它通过用自然语言书写非程序员可读的测试用例扩展了 测试驱动开发方法(TDD)。这让开发者得以把精力集中在代码应该怎么写,而不是技术细节上,而且也最大程度的减少了将代码编写者的技术语言与商业客户、用户、利益相关者、项目管理者等的领域语言之间来回翻译的代价。


3、方法

总结来说,测试用例可以按以下三步执行:
1. Given:配置测试的初始状态
2. When:对要测试的目标执行代码
3. Then:对测试结果进行断言(成功 or 失败)

在 given 节,应该给出要计算的值:在这里,我们给出了一个猜测数,你可以指定它和 targetValue 相差多少。
在when节,执行要测试的代码,调用gameUnderTest.check(_:)方法。
在 then 节,将结果和你期望的值进行断言(这里,gameUnderTest.scoreRound 应该是 100-5),如果测试失败,打印指定的消息。

  // 用 XCTAssert 测试模型
  func testScoreIsComputed() {
    // 1. given
    let guess = gameUnderTest.targetValue + 5
    // 2. when
    _ = gameUnderTest.check(guess: guess)
    // 3. then
    XCTAssertEqual(gameUnderTest.scoreRound, 95, "Score computed from guess is wrong")
  }

Given-When-Then 结构源自 BDD(行为驱动开发),是一个对客户端友好的、更少专业术语的叫法。另外也可以叫做 Arrange-Act-Assert 和 Assemble-Activate-Assert。


3、选型

XCTest + OCMock + OCHamcrest是我认为比较好的框架方案。

这里写图片描述


四、XCTest

1、创建

创建工程时选择Unit Tests

这里写图片描述

创建之后可以看到工程下面多了一个叫UnitTestDemoTests的部分,Targets也多了一个UnitTestDemoTests。

这里写图片描述

如果工程不包含,可以直接在Targets中添加Unit Tests。


2、用法

快捷键 Command + U。

查看UnitTestDemoTests.m中的代码

#import <XCTest/XCTest.h>

@interface UnitTestDemoTests : XCTestCase

@end

@implementation UnitTestDemoTests

- (void)setUp {
    [super setUp];
    // Put setup code here. This method is called before the invocation of each test method in the class.
}

- (void)tearDown {
    // Put teardown code here. This method is called after the invocation of each test method in the class.
    [super tearDown];
}

- (void)testExample {
    // This is an example of a functional test case.
    // Use XCTAssert and related functions to verify your tests produce the correct results.
}

- (void)testPerformanceExample {
    // This is an example of a performance test case.
    [self measureBlock:^{
        // Put the code you want to measure the time of here.
    }];
}

@end

我们可以看到有四个函数。
- (void)setUp
初始化的代码,在测试方法调用之前调用

- (void)tearDown
释放测试用例的资源代码,每个方法在每个测试用例执行后调用。测试失败不调用。

- (void)testExample
测试用例

- (void)testPerformanceExample
性能测试用例


3、规范

  • 测试方法以test打头

测试方法都写成“ - (void)testXXX ”形式,XXX表示要测试的方法名,并且无返回类型。

  • 测试类都应该以Tests结尾

如:UnitTestDemoTests


4、执行顺序

可以看到无论我们怎样调换test方法的书写顺序,其测试顺序都是不变的。

//
//  UnitTestDemoTests.m
//  UnitTestDemoTests
//
//  Created by 李兴东 on 2018/2/4.
//  Copyright © 2018年 xingshao. All rights reserved.
//

#import <XCTest/XCTest.h>

@interface UnitTestDemoTests : XCTestCase

@end

@implementation UnitTestDemoTests

- (void)setUp {
    [super setUp];
    //    初始化的代码,在测试方法调用之前调用
    // Put setup code here. This method is called before the invocation of each test method in the class.
}

- (void)tearDown {
    //    释放测试用例的资源代码,每个方法在每个测试用例执行后调用。测试失败不调用。
    // Put teardown code here. This method is called after the invocation of each test method in the class.
    [super tearDown];
}

- (void)testExample {
    //    测试用例
    // This is an example of a functional test case.
    // Use XCTAssert and related functions to verify your tests produce the correct results.
}

- (void)testa001 {
}

- (void)testa002 {
}

- (void)testa003 {
}

- (void)testA001 {
}

- (void)testA002 {
}

- (void)testA003 {
}

- (void)testB001 {
}

- (void)testC002 {
}

- (void)testD003 {
}

- (void)testPerformanceExample {
    //    性能测试用例
    // This is an example of a performance test case.
    [self measureBlock:^{
        //        需要测试性能的代码
        // Put the code you want to measure the time of here.
    }];
}

@end

输出顺序:

Test Case '-[UnitTestDemoTests testA001]' started.
Test Case '-[UnitTestDemoTests testA001]' passed (0.001 seconds).
Test Case '-[UnitTestDemoTests testa001]' started.
Test Case '-[UnitTestDemoTests testa001]' passed (0.000 seconds).
Test Case '-[UnitTestDemoTests testA002]' started.
Test Case '-[UnitTestDemoTests testA002]' passed (0.000 seconds).
Test Case '-[UnitTestDemoTests testa002]' started.
Test Case '-[UnitTestDemoTests testa002]' passed (0.007 seconds).
Test Case '-[UnitTestDemoTests testa003]' started.
Test Case '-[UnitTestDemoTests testa003]' passed (0.001 seconds).
Test Case '-[UnitTestDemoTests testA003]' started.
Test Case '-[UnitTestDemoTests testA003]' passed (0.001 seconds).
Test Case '-[UnitTestDemoTests testB001]' started.
Test Case '-[UnitTestDemoTests testB001]' passed (0.001 seconds).
Test Case '-[UnitTestDemoTests testC002]' started.
Test Case '-[UnitTestDemoTests testC002]' passed (0.001 seconds).
Test Case '-[UnitTestDemoTests testD003]' started.
Test Case '-[UnitTestDemoTests testD003]' passed (0.001 seconds).
Test Case '-[UnitTestDemoTests testExample]' started.
Test Case '-[UnitTestDemoTests testExample]' passed 

目前初步的结论:测试方法执行的顺序与方法名中test后面的字符大小有关,例如testA,testa,testB1,testB2三个方法相继执行。


5、断言

XCTFail(format…) 生成一个失败的测试;

XCTAssertNil(a1, format…)为空判断,a1为空时通过,反之不通过;

XCTAssertNotNil(a1, format…)不为空判断,a1不为空时通过,反之不通过;

XCTAssert(expression, format…)当expression求值为TRUE时通过;

XCTAssertTrue(expression, format…)当expression求值为TRUE时通过;

XCTAssertFalse(expression, format…)当expression求值为False时通过;

XCTAssertEqualObjects(a1, a2, format…)判断相等,[a1 isEqual:a2]值为TRUE时通过,其中一个不为空时,不通过;

XCTAssertNotEqualObjects(a1, a2, format…)判断不等,[a1 isEqual:a2]值为False时通过,

XCTAssertEqual(a1, a2, format…)判断相等(当a1和a2是 C语言标量、结构体或联合体时使用,实际测试发现NSString也可以);

XCTAssertNotEqual(a1, a2, format…)判断不等(当a1和a2是 C语言标量、结构体或联合体时使用);

XCTAssertEqualWithAccuracy(a1, a2, accuracy, format…)判断相等,(double或float类型)提供一个误差范围,当在误差范围(+/-accuracy)以内相等时通过测试;

XCTAssertNotEqualWithAccuracy(a1, a2, accuracy, format…) 判断不等,(double或float类型)提供一个误差范围,当在误差范围以内不等时通过测试;

XCTAssertThrows(expression, format…)异常测试,当expression发生异常时通过;反之不通过;(很变态)

XCTAssertThrowsSpecific(expression, specificException, format…) 异常测试,当expression发生specificException异常时通过;反之发生其他异常或不发生异常均不通过;

XCTAssertThrowsSpecificNamed(expression, specificException, exception_name, format…)异常测试,当expression发生具体异常、具体异常名称的异常时通过测试,反之不通过;

XCTAssertNoThrow(expression, format…)异常测试,当expression没有发生异常时通过测试;

XCTAssertNoThrowSpecific(expression, specificException, format…)异常测试,当expression没有发生具体异常、具体异常名称的异常时通过测试,反之不通过;

XCTAssertNoThrowSpecificNamed(expression, specificException, exception_name, format…)异常测试,当expression没有发生具体异常、具体异常名称的异常时通过测试,反之不通过


6、宏

static NSString *const NIM_TEST_NOTIFY_KEY = @"nim_test_notification";
#define WAIT_WITH_KEY(key) do{\
[self expectationForNotification:(key) object:nil handler:nil];\
[self waitForExpectationsWithTimeout:30 handler:nil];\
}while(0);

#define NOTIFY_WITH_KEY(key) do{\
dispatch_async(dispatch_get_main_queue(), ^{ \
[[NSNotificationCenter defaultCenter] postNotificationName:(key) object:nil];\
});\
}while(0);


#define WAIT       WAIT_WITH_KEY(NIM_TEST_NOTIFY_KEY)
#define NOTIFY     NOTIFY_WITH_KEY(NIM_TEST_NOTIFY_KEY)

7、应用

验证排序算法

- (void) testArraySorting {
    // Given
    NSArray *input = @[@1, @7, @6, @3, @10];
    // When
    NSArray *output = [input sortedArrayUsingSelector:@selector(compare:)];
    // Then
    NSArray *expect = @[@1, @3, @6, @7, @10];

//    不能如此验证  
//    XCTAssertEqual(output, expect);

    XCTAssertTrue(output.count == expect.count);

    [output enumerateObjectsUsingBlock:^(id  _Nonnull outObj, NSUInteger idx, BOOL * _Nonnull stop) {
        id expectObj = expect[idx];
        XCTAssertEqual(outObj, expectObj);
    }];

}

五、OCMock

1、安装

下载地址
静态库安装指南

接来下说一下framework的安装。
解压缩dmg得到OCMock.framework;
将OCMock.framework添加到工程中;
General->Embedded Binaries添加OCMock.framework;
Build Setting->Framework Search Paths 填入$(PROJECT_DIR)/$(TARGET_NAME)
Build Setting->Other Link Flags 填入-ObjC


2、用法

使用手册

1>创建对象

id classMock = OCMClassMock([SomeClass class]);
id protocolMock = OCMProtocolMock(@protocol(SomeProtocol));
id observerMock = OCMObserverMock();

2>置换方法

OCMStub([mock someMethod]).andReturn(anObject);
OCMStub([mock someMethod]).andCall(anotherObject, @selector(aDifferentMethod));
OCMStub([mock someMethodWithBlock:([OCMArg invokeBlockWithArgs:@"First arg", nil])]);

3>验证作用

id mock = OCMClassMock([SomeClass class]);

/* run code under test */

OCMVerify([mock someMethod]);
id mock = OCMClassMock([SomeClass class]);
OCMStub([mock someMethod]).andReturn(myValue);

/* run code under test */

OCMVerify([mock someMethod]);

4>参数约束

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

3、优点

Mock Object的行为简单,简单到唯一,在set up好返回值后总是返回这个唯一值。

Mock Object的行为可以预期,调用到你不希望调用的方法会让测试失败,方法被调用了你还可以验证其参数。(TurtleMock和EasyMock可以生成一个简单的NullObject的Mock Object,可以忽略对方法的非预期调用)

可以Mock一些在真实环境下难以模拟或出现的错误或异常。

Mock 是一种白盒测试工具(TurtleMock在你不去assert Mock Object的行为时不是),传统的Mock Object的set up过程就是目标代码实现细节的设计过程(TurtleMock的set up 过程是你构造Mock Object行为的过程,它并不关心目标代码的设计实现)。(这个其实很难说是优点,它所带来的问题也是很明显的,见Mock的弊端2)

接口为使用者设计,所以接口还未实现。Mock可以让你以简单的方式验证使用者是如何使用接口的。


3、弊端

Mock Object的行为依赖风险。Mock Object的行为和真实对象的行为必须一致,这在你对真实对象进行重构的时候是很大的风险。即使当前Mock Object的行为和真实Object的行为完全一直,而且所有测试都覆盖到了,其结果也很可能是:代码一处修改,测试到处失败。实际开发过程中这种情况 是非常 常见的,而且已经有不少人依赖这一点来修改代码了。其方法是先修改代码让所有依赖这块代码的测试都失败,然后再一点一点修改测试代码让测试通过,这看起来 还非常不错。

Mock Object的set up过程过于繁重。为此,大多数Mock Object的set up过程都会在TestCase的环境set up过程中进行,由于Mock Object的set up直接导致如何对该Mock Object进行verify,这使得你的TestCase的set up过程实际上也在进行测试,测试的内容不但多而且难以分割成小的TestCase。从另一个角度说,过于复杂的Mock Object 的set up过程,也许说明你的代码承担的职责过多,分出更多小的职责清晰的类也许可以让你避免这样的情况出现。在实际应用中,太多的情况是,在一个 TestCase中,set up Mock Object的代码比其它的代码多得多。也许正是使用Mock勉强能够测试你当前的设计,让你止步不前;而一旦TestCase建立完整,过多的Mock 验证又让你的重构寸步难行;最终导致你一闭眼一蹬腿,忍了。


4、范围

Mock我们仍然是需要的,在我们遇到如下问题时,Mock是我们的第一选择:
  外部资源,比如文件系统(Java的文件系统接口少,难以Mock,不过现在已经有不少开源项目专门做了内存的文件系统用于测试,比如cotta),这是因为对此类外部资源依赖性非常强,而其行为的不可预测性很可能导致测试的随机失败。
  UI,这个实际上和外部资源也搭得上边,因为UI很多时候需要用户行为触发事件。MVC和MVP模式都很好地解决了这个问题。
  第三方API
  当接口属于使用者,通过mock该接口来确定测试使用者与接口的交互,明确定义该接口的职责。
  在处理这些问题的过程中,特别是面对外部资源和第三方API时,Mock的风险是比较大的,多做Spike,为对应行为建立一组Acceptance测试是一个好的选择。
  显然在你建立的Domain内部,你不应该去想着用Mock,不应该去想该不该 用Mock,念头也不要动。你可以通过使用Observer去隔离对UI的依赖,通过Proxy去隔离对数据持久层的依赖,通过Adapter、 Proxy或者Stairway to heaven模式去隔离对第三方API的依赖,简单地说,Domain用到什么难以测试的外部包,使用接口隔离,把接口留在Domain里让依赖倒置,让 其它API去依赖Domain,提高你的Domain的独立性和可测试性,让你的代码真正为测试做足准备,从而在Domain里脱离Mock的苦海。


5、实战

XSPresenter文件

#import <Foundation/Foundation.h>
#import "XSModel.h"

@interface XSPresenter : NSObject

@property (nonatomic, strong) XSModel *model;
- (void)fetchDataWithIndexPath:(NSIndexPath *)indexPath withMarketType:(NSInteger)type success:(void(^)(XSModelData * responseObject))success fail:(void(^)(void))fail;

@end
#import "XSPresenter.h"

@interface XSPresenter()

@property (strong , nonatomic) NSMutableArray * cachelist;

@end

@implementation XSPresenter

- (instancetype)init{
    self = [super init];
    if (self) {
        _cachelist = [NSMutableArray array];
    }
    return self;
}

- (void)fetchDataWithIndexPath:(NSIndexPath *)indexPath withMarketType:(NSInteger)type success:(void(^)(XSModelData * responseObject))success fail:(void(^)(void))fail{

    XSModelData * resonpseModel = [self getCache];
    if (resonpseModel) {
        if (success) {
            success(resonpseModel);
        }
        return;
    }

    NSObject * model = [NSObject new];

    __typeof(self) __weak weakSelf = self;
    [self.model getData:model success:^(XSModelData *responseObject) {
        //缓存起来,避免tableView性能问题
        [weakSelf.cachelist addObject:responseObject];

        if (success) {
            success(responseObject);
        }

    } fail:^() {
        if (fail) {
            fail();
        }

    }];
}

- (XSModelData *) getCache{
    return [_cachelist firstObject];
}

@end

XSModel文件

#import <Foundation/Foundation.h>

@interface XSModelData: NSObject

@property (nonatomic, copy)NSString *name;

@end

@interface XSModel : NSObject

- (void) getData:(NSObject *)params
         success:(void(^)(XSModelData * responseObject))success
            fail:(void(^)(void))fail;
@end
#import "XSModel.h"

@implementation XSModelData
@end


@implementation XSModel

- (void)getData:(NSObject *)params success:(void(^)(XSModelData * responseObject))success fail:(void(^)(void))fail{
    if (params) {
        if (success) {
            XSModelData *data = [XSModelData new];
            data.name = NSStringFromClass([params class]);
            success(data);
        }
    }else{
        if (fail) {
            fail();
        }
    }
}

@end

实战验证
首先添加引用

@interface FZNDHomePresenterTests : XCTestCase

@property (nonatomic, strong) XSPresenter *presenter;
@property (nonatomic, strong) id model;

@end

生命周期

- (void)setUp {
    [super setUp];
    // Put setup code here. This method is called before the invocation of each test method in the class.
    self.presenter = [XSPresenter new];
    self.model = OCMClassMock([XSModel class]);
    self.presenter.model = self.model;
}

- (void)tearDown {
    // Put teardown code here. This method is called after the invocation of each test method in the class.
    self.model = nil;
    self.presenter = nil;
    [super tearDown];
}

验证presenter调用fetchDataWithIndexPath方法时,触发了model的getData方法

- (void)testVerifyFetchDataWithIndexPath {
    NSIndexPath *indexPath = OCMClassMock([NSIndexPath class]);
    [self.presenter fetchDataWithIndexPath:indexPath withMarketType:0 success:^(XSModelData *responseObject) {
        OCMVerify([self.model getData:OCMOCK_ANY success:OCMOCK_ANY fail:OCMOCK_ANY]);
    } fail:^() {
        OCMVerify([self.model getData:OCMOCK_ANY success:OCMOCK_ANY fail:OCMOCK_ANY]);
    }];
}

验证presenter调用fetchDataWithIndexPath成功时,返回结果为model中的成功结果

- (void)testFetchDataWithIndexPath{
    OCMStub([self.model getData:OCMOCK_ANY success:OCMOCK_ANY fail:OCMOCK_ANY]).
    andDo(^(NSInvocation *invocation){
        void(^successResponse)(XSModelData *response);
        [invocation getArgument:&successResponse atIndex:3];
        XSModelData *testResponse=[XSModelData new];
        testResponse.name = @"testString";
        successResponse(testResponse);
    });

    NSIndexPath *indexPath = OCMClassMock([NSIndexPath class]);
    [self.presenter fetchDataWithIndexPath:indexPath withMarketType:0 success:^(XSModelData *responseObject) {
        XCTAssertNotNil(responseObject);
        XCTAssertTrue([responseObject.name isEqualToString:@"testString"]);
    } fail:^() {
    }];
}

验证presenter调用fetchDataWithIndexPath缓存相关逻辑

- (void)testFetchDataWithIndexPathCache{

    OCMExpect([self.model getData:OCMOCK_ANY success:OCMOCK_ANY fail:OCMOCK_ANY]).
    andDo(^(NSInvocation *invocation){
        void(^success)(XSModelData *response);
        [invocation getArgument:&success atIndex:3];
        XSModelData *testResponse=[XSModelData new];
        testResponse.name = @"testString";
        success(testResponse);
    });

    NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
    [self.presenter fetchDataWithIndexPath:indexPath withMarketType:0 success:^(XSModelData *responseObject) {

        OCMVerify([self.model getData:OCMOCK_ANY success:OCMOCK_ANY fail:OCMOCK_ANY]);

        OCMReject([self.model getData:OCMOCK_ANY success:OCMOCK_ANY fail:OCMOCK_ANY]);

        [self.presenter fetchDataWithIndexPath:indexPath withMarketType:0 success:^(XSModelData *responseObject) {
        } fail:^() {
        }];

    } fail:^() {
    }];
    OCMVerifyAll(self.model);
}

GitHub

demo地址:https://github.com/SilenceLee17/UnitTestDemo


Question

1、UI Test的必要性?
Q:为什么不用 UI 测试?
A:
* 耗时长。特别是需要运行多个 case 的时候
* 无法测试内部的具体逻辑,比如 URL 是否正确


摘要

http://blog.csdn.net/jymn_chen/article/details/21552941
http://blog.csdn.net/jymn_chen/article/details/21562869
http://www.cnblogs.com/dokaygang128/p/3520761.html
http://www.cocoachina.com/ios/20150702/12253.html
https://www.jianshu.com/p/8bbec078cabe
http://www.cocoachina.com/ios/20170718/19930.html
http://xiangwangfeng.com/2016/10/17/走出-iOS-单元测试的困境/
https://colin1994.github.io/2017/08/31/iOS-Unit-Testing/
https://developer.apple.com/library/content/documentation/DeveloperTools/Conceptual/testing_with_xcode/chapters/01-introduction.html#//apple_ref/doc/uid/TP40014132-CH1-SW1
http://www.dotnetcurry.com/patterns-practices/1078/unit-testing-myths-best-practicess
https://www.jianshu.com/p/254d6f9f0bc4
http://www.cocoachina.com/ios/20150511/11768.html
http://blog.csdn.net/mmp591/article/details/78633250
http://www.51testing.com/html/26/n-3721726.html
http://blog.csdn.net/baihuaxiu123/article/details/51357364
https://github.com/yangyubo/zh-unit-testing-guidelines
http://www.infoq.com/cn/news/2007/04/google-testing-tips
http://googletesting.blogspot.com
http://www.ibm.com/developerworks/cn/linux/l-tdd/
http://www.infoq.com/cn/news/2008/02/unit_tests_forests_n_trees
http://www.infoq.com/cn/articles/thoughtworks-practice-partvi
http://blog.csdn.net/agilelee/article/details/6619482
http://www.infoq.com/cn/news/2009/05/recommended-tdd-tutorials
http://www.51testing.com/html/38/n-133138.html
http://www.cocoachina.com/ios/20150508/11769.html
http://ocmock.org
https://blog.csdn.net/agilelee/article/details/6619482

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值