写点什么

iOS 开发中的单元测试(二)——让断言活泼起来的匹配引擎

  • 2013-06-21
  • 本文字数:4902 字

    阅读完需:约 16 分钟

上一篇文章简单介绍了OCUnit 和GHUnit 两款iOS 开发中较为常见的单元测试框架,本文进一步介绍单元测试中的另一利器——匹配引擎(Matcher Engine)。匹配引擎可以替代断言方法,配合单元测试引擎使用,测试用例可以更多样化,更细致。

传统断言提供的方法数量和功能都有限,以导读中提到的两款框架为例,即使是断言相对丰富的GHUnit 也只是提供了38 种断言方法,范围仅涵盖了逻辑比较,异常和出错等少数几方面,仍然很单一。而使用匹配引擎代替断言,可能性就大大丰富了,除了普通断言支持的规则,一般的引擎还默认提供了包含,区间,继承关系等。更重要的是,使用匹配引擎开发者可以自行开发匹配规则,引入与业务相关的逻辑判断。

本文要介绍两款匹配引擎,一款就是Hamcrest 的Objective-C 实现——OCHamcrest,另一款则是专为Objective-C/Cocoa 而生的后来者——Expecta。接下来将结合GHUnitTest,介绍两款匹配引擎如何在单元测试中发挥作用(有关GHUnitTest 参考《iOS 开发中的单元测试(一)》

OCHamcrest

介绍匹配引擎必须要提 Hamcrest ,几乎已经成为匹配引擎的代名词。官网首页上的一句话表明了它的身世:“Born in Java, Hamcrest now has implementations in a number of languages.”。这款诞生于 Java 的匹配引擎现在还支持除 Java 的 Python、Ruby、PHP、Erlang 和 Objective-C。

  • 加入工程

在 iOS 工程中使用 OCHamcrest 需要先获取 OCHamcrestIOS.framework,可以从 Quality Coding 直接下载,或在 Github 上获取源码编译。注意:Github 上托管的 OCHamcrest 工程以 Submodule 的形式关联源代码,因此如果使用命令行方式 clone 工程,需要执行“git submodule update --init”。

下载源码后,进入 Source 目录,执行 MakeDistribution.sh 脚本,将会在 Source/build/Release 下生成 OCHamcrest.framework、OCHamcrestIOS.framework 和 OCHamcrest.framework.dSYM , OCHamcrestIOS.framework 就是 iOS 工程中需要用到的框架,如图 1。

图 1,从源码编译生成 OCHamcrestIOS.framework

打开已经安装了 GHUnitTest 的工程,把 OCHamcrestIOS.framework 添加到单元测试的 Target 中。在需要使用匹配引擎的用例中,定义“HC_SHORTHAND”并导入“<OCHamcrestIOS/OCHamcrestIOS.h>”(如图 2)。

图 2,把 OCHamcrestIOS.framework 导入工程

至此 OCHamcrest 已经安装完成,可以再测试用例中使用匹配规则代替 GHUnitTest 的断言方法。

  • 预定义规则

OCHamcrest 针对不同的数据类型提供了大量的预定义匹配规则,大大丰富了断言的类型。支持的数据类型包括:对象、容器、数值和文本,此外还提供了专门的逻辑匹配规则。

以文本(一般就是 NSString)为例,OCHamcrest 提供了 6 种针对对象的匹配规则:

IsEqualIgnoringCase,该文本是否与给出的文本相同(忽略大小写);

IsEqualIgnoringWhiteSpace,该文本是否与给出的文本相同(忽略空格);

StringContains,该文本是否包含给出的文本片段;

StringContainsInOrder,该文本是否按照先后顺序包含给出的若干文本片段;

StringEndsWith,该文本是否以给出的文本片段结尾;

StringStartsWith,该文本是否以给出的文本片段开头。

另外,再举 OCHamcrest 为对象(NSObject 和 NSObject 的子类)预定义的 8 条规则:

ConformsToProtocol,该对象是否遵循了给出的协议,或者说是否实现了给出的 Delegate;

HasDescription,允许使用文本规则对给出的一段文本与该对象的描述进行匹配;

HasProperty,该对象是否含有给出的属性;

IsInstanceOf,是给出的类的实例,或是给出的类子类的实例;

IsTypeOf,是给出的类的实例,不同于 IsInstanceOf,无法匹配子类实例;

IsNil,为空;

IsSame,与给出的对象是同一个实例。

  • 撰写用例

OCHamcrest 提供了匹配规则和相应的断言方法,配合单元测试框架(本文以 GHUnit 为例, 在《iOS 开发中的单元测试(一)》中已经介绍了如何安装GHUnit 框架并撰写用例)的驱动机制即可撰写用例。本文以联合使用上述提到的StringStartsWith 和HasDescription 规则为例。

首先,定义一个用于示例的类“Man”(如图3),有属性friends,当friends 为空,其description 为“Man without any friend, so sorry.”,反之为“Nice persion with [friends count] friend(s).”。(使用Foo 或Bar 这样的示例会显得很没情怀吧 ;-|)

图3,用于测试的类:Man

用例中判断某Man 实例的description 是否以Nice 开头(这是不是一个友善的人),如图4。

图4,测试用例两则

UntTestCase 是 GHTestCase 的子类,引入 <OCHamcrestIOS/OCHamcrestIOS.h> 并定义 HC_SHORTHAND 表示使用 OChamcrest。setUp 方法在每个测试方法执行之前初始化一个 Man 实例;testANiceMan 方法向 Man 实例的 friends 属性中加入两个值,因此该实例的 description 将返回“Nice man …”;使用 OCHamcrest 提供的断言方法 assertThat 与匹配规则配合,判断该实例的 description 是否以“Nice”开头;testNotANiceMan 方法则直接测试一个未经过加入 friends 的实例测试。

上述测试,testANiceMan 方法顺利通过,testNotANiceMan 不会通过,直接报出错误堆栈,并打印在匹配规则中预先定义好的出错信息(如图 5)。

图 5,测试结果

  • 辅助方法

Syntactic Sugar 是一种提高匹配规则和断言可读性的方案,让一个匹配和断言看起来更像是一句自然语言的话,而非多个函数的堆砌,对实际的匹配运算不产生任何影响。例如,没有加 Sugar 的匹配:

复制代码
assertThat(foo, equalTo(bar));

加 Sugar 可以是:

复制代码
assertThat(foo, is(equalTo(bar)));

除了 Sugar,OCHamcrest 还提供了 describedAs 方法,用于辅助断言方法,自定义出错文案,例如:

复制代码
assertThat(foo, describedAs(@”foo should be equal to bar”, equalTo(bar), nil));
  • 自定义规则

OCHamcrest 官方给出的自定义匹配规则示例是: onASaturday ,判断一个 NSDateComponents 实例是否为星期六。本文以上一节使用的 Man 对象为例,匹配某 Man 实例是否有一个名为“Joe”的好友,规则命名为:hasAFriendJoe。

自定义匹配规则包括两部分,一个 Macher 类和一个用 OBJC_EXPORT 方式定义的函数。

自定义 Macher 类都是 HCBaseMatcher 的子类(如图 6),接口中定义的类初始化方法供匹配方法 hasAFriendJoe 调用,其实现则通过调用接口中定义的另一个实例方法。

图 6,HasAFriend 接口和匹配方法 hasAFriendJoe 定义

在 HasAFriend 中需要引入 <OCHamcrestIOS/HCDescription.h>,并重写父类中的 matches: 和 describeTo: 方法(如图 7)。在 maches: 方法中实现匹配逻辑,匹配成功则返回 YES,否则返回 NO;describeTo 是失败后的描述;hasAFriendJoe 方法只需要调用类方法初始化匹配规则类即可。匹配规则定义后,可以配合断言方法使用,如上一节所示的 assertThat 方法:

assertThat(self.man, hasAFriendJoe());

图 7,规则实现

Expecta

Expecta 专为 Objective-C/Cocoa 而生,相比 OCHamcrest,其优化了匹配的语法,测试用例的可读性更高。此外,Expecta 对匹配对象类型没有强制要求,允许任意类型的数据进行匹配。在 OCHamcrest 中每一条匹配规则都是一个方法,规则联合使用也需要以参数形式传递。在 Expecata 中联合规则的语法是以点号连接,借助 Sugar 介词可以把一个联合规则拼装成一句符合自然语法的句子,例如:

复制代码
OCHamcrest —— assertThat(@"foo", is(equalTo(@"foo")));
Expecta —— expect(@"foo").to.equal(@"foo");
  • 加入工程

Expecta 提供了 CocoaPods 的源,可以通过定义依赖引入:

复制代码
dependency 'Expecta', '~> 0.2.1'

或者从 github 上获取源代码,编译出 Library,引入 XCode 工程。下载源码后,进入工程目录,运行 rake,编译工程。编译完成后,把 products 目录拷贝到工程中(如图 8),在 iOS/MacOSX 工程中加入响应的.a 文件(如图 9)。在 Build Settings 的 Other Linker Flags 中加入 -ObjC 参数(在《iOS 开发中的单元测试(一)》中添加 GHUnit 一节介绍了如何添加 -ObjC 的参数)。与 OCHamcrest 类似,在测试用例中定义 EXP_SHORTHAND,并引入“Expecta.h”(如图 10)。

图 8,加入编译后的头文件列表

图 9,加入 Library 文件

图 10,用例中引入 Expecta

  • 预定义规则

Expecta 提供的预定义规则只有 20 条,远远少于 OCHamcrest 提供的预定义规则。由于 Expecta 的匹配规则对匹配对象没有要求,因此没有提供像 OCHamcrest 中针对某种对象的特定规则。

在 Expecta 的 github 首页可以看到全部预定义规则列表。举几个较有特点的规则为例:

复制代码
expect(x).to.beCloseToWithin(y, z),x 距离 y 的距离小于 z
expect(x).to.beTruthy(), x 是否为真(或非空);
expect(x).to.beFalsy(),x 是否为假(或空 / 零);
expect(^{ /* code */ }).to.raiseAny(),该 Block 是否抛出异常;
expect(^{ /* code */ }).to.raise(@"ExceptionName"),该 Block 是否抛出给定名字异常。

此外,通过.notTo 或.toNot 对规则取反进行匹配,如:expect(x).notTo.equal(y)。

通过.will 或.willNot 进行异步匹配,即在超时时间(默认超时 1 秒,也可通过 [Expecta setAsynchronousTestTimeout:x] 设定)之前满足匹配规则即可,如:expect(x).will.beNil()。

  • 撰写用例

Expecta 不使用类似 assertThat 类似的辅助断言方法,而是直接使用 expecta. 语法匹配。

仍然以 GHUnit 测试用例为例,测试一个数字 n,是否在 5 附近,距离小于 2,即处在 [3,7] 区间内(如图 11)。

图 11,Expecta 测试用例

Expecta 不支持匹配规则的联合使用。

  • 辅助方法

Expecta 也有语法 Sugar:to。

  • 自定义规则

Expecta 的自定义规则有两种方式,静态规则和动态规则。

定义静态规则:

Expecta 的匹配规则不是一个类,是通过框架提供的宏定义来实现的,操作比定义 OCHamcrest 规则简单不少。仍以 OCHamcrest 中的判断一个 Man 实例是否有名为“Joe”的好友。

通过 EXPMatcherInterface() 方法定义,该方法有两个参数,规则名和规则参数列表。示例如图 12。

图 12,扩展规则 hasAFriendJoe 定义

EXPMacherInterface 第二个参数允许通过这样的方式定义列表:(NSString *Foo, int bar)。

在实现中,用 EXPMatcherImplementationBegin 和 EXPMatcherImplementationEnd 标示规则实现的头尾。并定义 prerequisite、match、failureMessageForTo 和 failureMessageForNotTo 四个 Block,分别返回与判断结果,匹配结果,正向匹配出错原因和反相匹配出错原因(如图 13)。由于 Expecta 框架不支持 ARC,因此需要在 Build Settings 中对该.m 文件添加 -fno-objc-arc 参数。在测试用例中可以通过如下语法调用:

复制代码
expect(self.man).hasAFriend(@"Joe");

或反相匹配:

复制代码
expect(self.man).notTo.hasAFriend(@"Joe");

图 13,Expecta 自定义规则实现

定义动态规则:

动态规则是本质上并不是一段逻辑匹配,而是通过 Expecta 的语法对匹配对象的属性进行是否为真的断言。例如:

复制代码
@interface LightSwitch : NSObject
@property (nonatomic, assign, getter=isTurnedOn) BOOL turnedOn;
@end
@implementation LightSwitch
@synthesize turnedOn;
@end

可以写出如下断言:

复制代码
expect([lightSwitch isTurnedOn]).to.beTruthy();

建立动态规则:

复制代码
EXPMatcherInterface(isTurnedOn, (void));

就可以通过以下断言判断 turendOn 属性的真假:

复制代码
expect(lightSwitch).isTurnedOn();

总结

整体看两款匹配引擎,Expecta 小巧,敏捷,提供了多种灵活的匹配方式,OCHamcrest 从 Hamecrest 体系继承而来,形式更加中庸,提供的机制更完善。从开发者的角度看,Expecta 更好玩,而 OCHamcrest 更实用,在实验性的项目中我会偏向选择 Expecta,而较正式的项目则会使用 OCHamcrest。

OCHamcrest 结合上一篇《iOS 开发中的单元测试(一)》中介绍的单元测试框架GHUnit 可以给开发者提供一个完整的单元测试方案,建议开发者在自己的项目中引入这样的质量自控机制,写出健壮的代码。

通过两篇文章介绍了单元测试框架和匹配引擎的一些基础知识,在接下来的文章中,我将结合一个项目,从实战角度详细记述如何开发带有单元测试的iOS 项目。

作者简介:

高嘉峻(微博: @gaosboy ),SegmentFault.com 联合创始人,杭州 iOS 开发者沙龙发起人,资深 iOS 开发者。


感谢李永伦对本文的审校。

2013-06-21 05:135946

评论

发布
暂无评论
发现更多内容

JeecgBoot v3.8.1发布,集成AI应用的低代码平台

JEECG低代码

低代码平台 代码生成 AIGC JeecgBoot AI低代码平台

巅峰对决,超三十万奖金等你挑战!第十届信也科技杯全球AI算法大赛火热开赛!

袁袁袁袁满

AI

PDF Expert for Mac —— 专业 PDF 编辑阅读转换工具

柠檬与橘子

稳定币发行量创纪录地超过 Visa 交易量

TechubNews

解开堡垒机的神秘面纱之堡垒机是什么软件?

行云管家

网络安全 数据安全 堡垒机

烘焙工业4.0:自动化升级与制造执行MES系统的深度赋能

万界星空科技

mes 万界星空科技mes 食品MES 食品加工行业 烘焙行业

打破志愿填报信息差:华为浏览器推出小艺高考Agent一站式免费问答服务

最新动态

如何快速打印CAD图纸?怎么设置?

在路上

cad cad看图 CAD看图王

吉利银河A7全球首秀,2L级油耗引领电混家轿进入新时代

极客天地

文档解析工具API表格提取完全指南:从坐标迷雾到精准裁剪

合合技术团队

人工智能 算法 #大数据

开源鸿蒙持续壮大 三大运营商全面入局 多元成果亮相HDC2025

最新动态

现代财务——智能技术背景下的企业变革

智达方通

企业管理 全面预算 财务管理

Python搭建HTTP服务如何快速远程访问?内网穿透工具一招搞定!

贝锐

Python HTTP 内网穿透

如何安装CST软件拓展资料包

思茂信息

cst CST软件 CST Studio Suite

Community Over Code Asia 2025DataOps专题:SeaTunnel社区出品并将带来多个分享!

白鲸开源

开源 技术 数据集成 Apache SeaTunnel CommunityOverCode

昆明卷烟厂引入时序数据库 TDengine,支撑百万点位高频数据实时处理

TDengine

tdengine 时序数据库 国产时序数据库 时序数据库tdengine

Dify 开发者必看:如何破解 MCP 集成与 Prompt 迭代难题?

阿里巴巴云原生

阿里云 云原生 nacos

Spring状态机在项目中的妙用,太优雅了!

Geek_e3e86e

Java 编程

《Building REST APIs with Flask》读后感

codists

flask

淘宝API系列:淘宝商品详情接口详解

tbapi

淘宝商品详情接口 淘宝API 天猫商品详情接口

重塑零售科技:第七在线如何利用人工智能优化商品销售

第七在线

WebGL 开发数字图书馆的技术方案

北京木奇移动技术有限公司

软件外包公司 数字孪生开发 webgl开发

无需公网IP,内网穿透远程访问Ollama+Gemma3模型+Open WebUI教程

贝锐

内网穿透 ollama Gemma

这家公司使用 MCP,已向企业交付 1000 名数字员工

阿里巴巴云原生

阿里云 云原生 Higress MCP

三类不同身份的“开发者”,在昇腾CANN的土壤上种下了繁荣之花

极客天地

全球第一!百度文库智能PPT月访问量超3400万,用户规模翻倍增长

极客天地

这个提升效率宝藏级工具一定要收藏使用

秃头小帅oi

产业的尽头是AI,云南交投智算中心让智慧交通跑出加速度

Alter

AI 智能体

海洋服务业定义以及需要使用堡垒机场景分析

行云管家

网络安全 数据安全

CAD图层是干什么用的?怎么设置?

在路上

cad

基于 AI 网关和 llmaz,提升 vLLM 推理服务可用性和部署易用性的实践

阿里巴巴云原生

阿里云 云原生 Higress

iOS开发中的单元测试(二)——让断言活泼起来的匹配引擎_Android/iOS_高嘉峻_InfoQ精选文章