Xcode 5 单元测试(一)使用XCTest进行单元测试中说了如何在Xcode 5中使用XCTest进行简单的单元测试,本文就来探讨下mock测试和更高级的工具GHUnit。

Mock

首先科普下什么是mock测试。mock测试是个很神奇而又很酷的技术,在测试过程中,对于一些不容易构造或不容易获取的对象,此时你可以创建一个虚拟的对象(mock object)来完成测试。

例如你可能要尝试100次才会返回一个NSError,通过mock object你可以自行创建一个NSError对象,测试在出错情况下程序的处理是否符合你的预期。

例如你要连接服务器但是服务器在实验室,你在外工作的时候就无法测试了(小弟就试过这种情况,非常反感),这个时候你可以创建一个虚拟的服务器,并返回一些你指定的数据,从而绕过服务器。

例如假设你要访问一个数据库,但是访问过程的开销巨大,这时你可以虚拟一个数据库,并且返回一些自行定制的数据,从而绕过了数据库的访问。

mock的思想很简单:没有条件?我们就自行创造条件。

OCMock

1.关于OCMock

OCMock是一个用于为iOS或Mac OS
X项目配置Mock测试的开源项目,如果目标是iOS项目那么生成的是静态库,如果是Mac OS
X项目生成的是框架。小弟粗略看过下OCMock的源码(可惜功力不够,目前只看了一小部分),其实现思想就是根据要mock的对象的class来创建一
个对应的对象,并且设置好该对象的属性和调用预定方法后的动作(例如返回一个值,调用代码块,发送消息等等),然后将其记录到一个数组中,接下来开发者主
动调用该方法,最后做一个verify(验证),从而判断该方法是否被调用,或者调用过程中是否抛出异常等。

在讲解如何在iOS项目中添加OCMock静态库之前,先给出OCMock的资料地址:

OCMock官网

iOS Project Setup:在iOS项目中配置OCMock的教程

erikdoe / ocmock:在GitHub上的示例项目,可以参考下其中的一些配置参数

OCMock Download:OCMock的静态库、框架和工程文件(可以在这里看OCMock的源码实现)下载地址,已经打包成dmg格式了。

2.配置OCMock

好吧,进入主题。还是以(一)中的UnitTestDemo那个工程为例吧。

1.下载OCMock Download的dmg文件,将iOS文件夹中的文件(libOCMock.a和OCMock文件夹)拷贝到要测试的项目目录下:

2.打开工程,首先添加以上文件到项目中(Command + Option + A):

3.打开UnitTestDemoTests Target的Build Phases,添加libOCMock.a到要链接的类库中:

4.打开Build Settings,搜索Other Linker Flags,设置如下:

这里的-ObjC表示告诉链接器,要把OC类和Category加载到工程中,但是该
设置有Bug,所以还要用-all_load或者-force_load来加载静态库中没有加载进来的Category。如果使用-all_load会把
所有相关无关的文件都load进来,使得目标程序变得更大,所以用-force_load来指定要加载的静态库就可以了,下面的"$(SRCROOT)/ocmock/libOCMock.a"就是静态库文件在Finder中的路径。

再搜索Header Search Paths,设置如下:

"$(SRCROOT)/ocmock"给出的是OCMock的头文件在Finder中的路径,因此该选项告诉编译器应该到哪里去寻找OCMock静态库的头文件。

3.编写mock测试

新建一个test case class类,基类为XCTestCase,命名为MockTableTests。

首先我们测试一下TableDataSource的numberOfRowsInSection方法是否返回了正确的值,测试代码如下:

  1. - (void)testNumberOfRows {
  2. // 1.创建Table View的DataSource
  3. TableViewCellConfigureBlock cellConfigureBlock = ^(UITableViewCell *cell, NSString *item) {
  4. cell.textLabel.text = item;
  5. };
  6. TableDataSource *tableSource = [[TableDataSource alloc] initWithItems:@[@"1", @"2", @"3"]
  7. CellIdentifier:@"foo"
  8. ConfigureCellBlock:cellConfigureBlock];
  9. // 2.创建mock table view
  10. id mockTableView = [OCMockObject mockForClass:[UITableView class]];
  11. // 3.断言
  12. XCTAssertEqual([tableSource tableView:mockTableView numberOfRowsInSection:0], (NSInteger)3, @"Mock table returns a bad number of rows in section 0");
  13. }

1.
首先创建data source,用于下文中调用numberOfRowsInSection方法。注意这里Table View中的内容@[@"1",
@"2", @"3"]是需要我们手动配置的。这里也体现了mock的一个局限性,就是mock
object的关键属性都要我们自己定制,如果要模拟的对象非常的大,那么创建一个mock object的成本将远远大于单元测试带来的效益。

2.如果要单独测试numberOfRowsInSection方法,我们就需要有一个TableView,因此要通过OCMockObject的mockForClass类方法来创建一个mock table view。

3.通过data source调用方法,并使用断言判断。

如果在测试时,我们只想在控制台中看见这个方法的输出信息,可以点击方法前面的一个小播放按钮:

控制台输出:

  1. Test Suite 'Multiple Selected Tests' started at 2014-03-20 01:58:17 +0000
  2. Test Suite 'UnitTestDemoTests.xctest' started at 2014-03-20 01:58:17 +0000
  3. Test Suite 'CellConfigureTests' started at 2014-03-20 01:58:17 +0000
  4. Test Suite 'CellConfigureTests' finished at 2014-03-20 01:58:17 +0000.
  5. Executed 0 tests, with 0 failures (0 unexpected) in 0.000 (0.000) seconds
  6. Test Suite 'MockTableTests' started at 2014-03-20 01:58:17 +0000
  7. Test Case '-[MockTableTests testNumberOfRows]' started.
  8. Test Case '-[MockTableTests testNumberOfRows]' passed (0.000 seconds).
  9. Test Suite 'MockTableTests' finished at 2014-03-20 01:58:17 +0000.
  10. Executed 1 test, with 0 failures (0 unexpected) in 0.000 (0.000) seconds
  11. Test Suite 'UnitTestDemoTests' started at 2014-03-20 01:58:17 +0000
  12. Test Suite 'UnitTestDemoTests' finished at 2014-03-20 01:58:17 +0000.
  13. Executed 0 tests, with 0 failures (0 unexpected) in 0.000 (0.000) seconds
  14. Test Suite 'UnitTestDemoTests.xctest' finished at 2014-03-20 01:58:17 +0000.
  15. Executed 1 test, with 0 failures (0 unexpected) in 0.000 (0.000) seconds
  16. Test Suite 'Multiple Selected Tests' finished at 2014-03-20 01:58:17 +0000.
  17. Executed 1 test, with 0 failures (0 unexpected) in 0.000 (0.001) seconds

下面来编写一个稍微复杂点的mock测试,用来测试UITableViewDataSource中的cellForRowAtIndexPath方法。代码如下:

  1. - (void)testCellConfiguration {
  2. // 1.创建Table data source
  3. __block UITableViewCell *configuredCell = nil;
  4. __block id configuredObject = nil;
  5. TableViewCellConfigureBlock block = ^(UITableViewCell *a, id b) {
  6. configuredCell   = a;
  7. configuredObject = b;
  8. };
  9. TableDataSource *dataSource = [[TableDataSource alloc] initWithItems:@[@"a", @"b"]
  10. CellIdentifier:@"foo"
  11. ConfigureCellBlock:block];
  12. // 2.创建mock table view
  13. id mockTableView = [OCMockObject mockForClass:[UITableView class]];
  14. // 3.设定mock table view的行为
  15. UITableViewCell *cell = [[UITableViewCell alloc] init];
  16. [[[mockTableView expect] andReturn:cell] dequeueReusableCellWithIdentifier:@"foo" forIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]];
  17. //    [[[mockTableView stub] andReturn:cell] dequeueReusableCellWithIdentifier:@"foo" forIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]];
  18. // 4.主动调用cellForRowAtIndexPath方法
  19. id result = [dataSource tableView:mockTableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]];
  20. // 5.验证mock table view的行为
  21. [mockTableView verify];
  22. // 6.断言
  23. XCTAssertEqual(result, cell, @"Should return the dummy cell.");
  24. XCTAssertEqual(configuredCell, cell, @"This should have been passed to the block.");
  25. XCTAssertEqualObjects(configuredObject, @"a", @"This should have been passed to the block.");
  26. }

1.创建Table data source,用于下文调用cellForRowAtIndexPath方法。

2.创建mock table view。

3.
如果mock table
view调用了dequeueReusableCellWithIdentifier:@"foo" forIndexPath:
[NSIndexPath indexPathForRow:0
inSection:0]]方法,那么就返回上面已经创建好的UITableViewCell对象,expect方法表示该方法必须被调用(见5.)。

4.通过Table data source主动调用cellForRowAtIndexPath方法,此时会触发mock table view调用dequeueReusableCellWithIdentifier:forIndexPath:方法。

5.
最后要调用verify方法,用于验证mock table view的行为。如果mock table
view在某个方法中调用了expect,那么该方法必须在verify之前被调用,否则测试无法通过。如果mock table
view调用的是stub,那么verify时OCMock并不关心该方法是否调用过,只会关心调用过程是否发生异常或有测试被拒绝等。

6.断言,在这里进行各种比较。

GHUnit

可能大家都注意到了,在运行测试后,控制台中的输出可以用惨不忍睹来形容。这时我们可以尝试另一个工具:GHUnit框架,这个工具是有GUI的。

首先给出一些参考资料:

gh-unit / gh-unit:该项目在GitHub上的地址。

Installing in iOS (Xcode 4):在Xcode 4上为项目配置GHUnit,相对Xcode 5来说旧了,但是安装过程还是类似的。

guide_testing Document:编写测试的参考文档。

还是以之前的那个项目为例,进行GHUnit的配置,并编写基于该框架的单元测试。

1.配置GHUnit

1.新建一个Target,选中空的工程模板:

2.选中目标工程为本工程:

将新Target中的冗余文件全部删掉,包括AppDelegate.h/.m,GHUnitTestsTests文件夹,GHUnitTestsTests Target等,注意Supporting Files和Images.xcassets要保留:

3.从GitHub中下载GHUnit项目,下载后可以看到有个Project-iOS目录:

然后打开我们可爱的终端,首先cd到Project-iOS目录下,然后输入make命令,成功后的部分提示如下:

  1. ** BUILD SUCCEEDED **
  2. BUILD_DIR="build" BUILD_STYLE="Release" sh ../Scripts/CombineLibs.sh
  3. adding: libGHUnitIOS.a (deflated 63%)
  4. sh ../Scripts/iOSFramework.sh
  5. Framework: Cleaning framework...
  6. Framework: Setting up directories...
  7. Framework: Creating symlinks...
  8. Framework: Creating library...
  9. Framework: Copying assets into current version...
  10. The framework was built at: build/Framework/GHUnitIOS.framework

可以看到GHUnit项目路径/Project-iOS/目录下多了个build文件夹,./build/Framework/下面生成的框架就是我们make的目标:

4.回到原来的工程,Command + Option + A,将生成的framework文件添加到工程中。

打开GHUnitTests Target的Build Settings,搜索Other Linker Flags,将其设置为-all_load和-ObjC:

5.打开main.m,删除#import “AppDelegate.h”

将主函数的return修改如下:

  1. return UIApplicationMain(argc, argv, nil, @"GHUnitIOSAppDelegate");

完成配置。

2.编写GHUnit单元测试

在GHUnitTests中新建一个Objective-C Class,基类为NSObject,名为FirstGHUnitTests。

将FirstGHUnitTests.h删除,修改FirstGHUnitTests.m代码如下:

  1. #import <GHUnitIOS/GHUnit.h>
  2. #import "TableDataSource.h"
  3. #import "TableViewController.h"
  4. @interface MyTest : GHTestCase
  5. @end
  6. @implementation MyTest
  7. - (void)testStrings {
  8. NSString *string1 = @"a string";
  9. GHTestLog(@"I can log to the GHUnit test console: %@", string1);
  10. // Assert string1 is not NULL, with no custom error description
  11. GHAssertNotNil(string1, nil);
  12. // Assert equal objects, add custom error description
  13. NSString *string2 = @"a string";
  14. GHAssertEqualObjects(string1, string2, @"A custom error message. string1 should be equal to: %@.", string2);
  15. }
  16. - (void)testSimpleFail {
  17. GHAssertTrue(NO, nil);
  18. }
  19. - (void)testDataSourceInitializing {
  20. TableViewCellConfigureBlock cellConfigureBlock = ^(UITableViewCell *cell, NSString *item) {
  21. cell.textLabel.text = item;
  22. };
  23. TableDataSource *tableSource = [[TableDataSource alloc] initWithItems:@[@"1", @"2", @"3"]
  24. CellIdentifier:@"TestCell"
  25. ConfigureCellBlock:cellConfigureBlock];
  26. GHAssertNotNil(tableSource, @"TableView data source should not be nil");
  27. }
  28. @end

由于TableDataSource.h和TableViewController.h属于另一个Target中的文件,所以这里我们要把对应的实现文件加入到本Target的Compile Sources中(否则会报Undefined symbols for architecture
i386: _OBJC_CLASS_$的错误):

在Scheme中选中GHUnitTests中的模拟器:

Command + R,可以看到一个测试界面。点击右上角的Run,就可以运行列表中的所有单元测试:

点击表格中的一行,可以查看测试信息(当然控制台也有输出):

对比起使用XCTest框架在控制台的输出好看多了。

Demo下载地址:点此进入下载页

小结

建议使用GHUnit +
OCMock组合进行单元测试,功能强大界面美观。当然也有人是用XCTest + xctool +
OCMock的,xctool是Facebook出品,应该也是非常强大的工具,但是小弟在机子上用brew一直装不上xctool(暂时还没找到解决方
法),所以这里就不说xctool的部分了。

其它参考资料:

XCode下的iOS单元测试

iOS进行单元测试OCUnit+xctool - yingkong1987

XCode下的iOS单元测试

GHUnit的使用

Xcode 5 单元测试(二)OCMock和GHUnit的更多相关文章

  1. Xcode 5 单元测试(一)使用XCTest进行单元测试

    在Objc.io #1的Testing View Controllers中讲解的就是单元测试的相关内容.本文说下如何通过Xcode 5中集成的XCTest框架进行简单的单元测试. 什么是单元测试 首先 ...

  2. Intellij Idea系列之导Jar包与编写单元测试(二)

     Intellij Idea系列之导Jar包与编写单元测试(二) 一.初衷 对于很多的初学者来说,Intellij如何导入jar包感到很迷惑,甚至在网上搜过相关文章之后还是云里雾里,本博客通过图文并茂 ...

  3. RIGHT-BICEP单元测试——“二柱子四则运算升级版”

    RIGHT-BICEP单元测试 ——“二柱子四则运算升级版” ”单元测试“这对于我们来说是一个全新的专业含义,在上了软件工程这门课,并当堂编写了简单的"求一组数中的最大值"函数的单 ...

  4. Django单元测试二三事

    零.前言 之前做过一个微信公众平台的开发者后台,功能比较简单,我个人也比较懒,所以就没有写测试.前段时间更新了一下版本,对代码进行了改动.结果昨天收到消息说后台出问题了,一个功能无法使用.我检查了半天 ...

  5. [Xcode 实际操作]二、视图与手势-(1)UIView视图的基本使用

    目录:[Swift]Xcode实际操作 本文将演示在视图控制器的根视图里添加两个视图对象. import UIKit class ViewController: UIViewController { ...

  6. [Xcode 实际操作]二、视图与手势-(2)UIView视图的层次关系

    目录:[Swift]Xcode实际操作 本文将演示创建三个视图对象,其中第二个视图是第三个视图的父视图. 现在开始编写代码,实现这项功能 import UIKit class ViewControll ...

  7. [Xcode 实际操作]二、视图与手势-(3)UIView视图的基本操作

    目录:[Swift]Xcode实际操作 本文将实现视图的添加与删除,以及切换视图在父视图中的层次. import UIKit class ViewController: UIViewControlle ...

  8. [Xcode 实际操作]二、视图与手势-(4)给图像视图添加边框效果

    目录:[Swift]Xcode实际操作 本文将演示给图片添加颜色相框 import UIKit class ViewController: UIViewController { override fu ...

  9. [Xcode 实际操作]二、视图与手势-(5)给图像视图添加圆角效果

    目录:[Swift]Xcode实际操作 本文将演示给矩形图片添加圆角效果 import UIKit class ViewController: UIViewController { override ...

随机推荐

  1. Flex学习笔记

    Flex —— Flexible Box 弹性布局 用来为盒子模型提供灵活性 /* 块级元素 */ .box{ display: flex; } /* 行内元素 */ .box{ display: i ...

  2. 《c程序设计语言》读书笔记-5.9-指针转换天数和日期

    #include "stdio.h" #include "stdlib.h" #include "string.h" static char ...

  3. bzoj [Noi2008] 1061 志愿者招募 单纯形

    [Noi2008]志愿者招募 Time Limit: 20 Sec  Memory Limit: 162 MBSubmit: 5437  Solved: 3267[Submit][Status][Di ...

  4. 转:LinkedHashMap使用(可以用来实现LRU缓存)

    1. LinkedHashMap概述: LinkedHashMap是HashMap的一个子类,它保留插入的顺序,如果需要输出的顺序和输入时的相同,那么就选用LinkedHashMap. LinkedH ...

  5. myeclipse 常规web项目创建

    配置jdk 我的jdk C:\Program Files\Java\jdk1.7.0_67    window --> preferences -->     Java --> In ...

  6. 汕头市队赛 SRM 09 C 撕书

    C 撕书III-3 SRM 09 背景&&描述 琉璃双在撕书.     书总共有n页,每页都可以看作是一个数字.     琉璃读书喜欢来回地读.但他也因此发现了作者的灌水行为:有些连续 ...

  7. 快来看看Google出品的Protocol Buffer,别只会用Json和XML了

    前言 习惯用 Json.XML 数据存储格式的你们,相信大多都没听过Protocol Buffer Protocol Buffer 其实 是 Google出品的一种轻量 & 高效的结构化数据存 ...

  8. Android 画笔Paint

    转自 http://wuxiaolong.me/2016/08/20/Paint/ 了解Android Paint,一篇就够.引用Aige<自定义控件其实很简单>系列博客的话“很多时候你压 ...

  9. [bzoj1026][SCOI2009]windy数——数位dp

    题目 求[a,b]中的windy数个数. windy数指的是任意相邻两个数位上的数至少相差2的数,比如135是,134不是. 题解 感觉这个题比刚才做的那个简单多了...这个才真的应该是数位dp入门题 ...

  10. ClientScript.RegisterClientScriptBlock 不执行

    ClientScript.RegisterClientScriptBlock 不执行 页面中 form标签必须加入 runat=server