OC单元测试

转载自你应该知道的单元测试

一、单元测试

单元测试,一个不断被强调,又不断被人忽略的话题,想从屌丝程序员晋级成高级工程师,单元测试,可以说是必不可少的技能。如何编写合适的测试用例?何时该进行单元测试?单元测试所体现的价值究竟是什么?可以说,有很多实际的困扰阻碍着一批人,使得这些人被卡在了单元测试的门外,万事起步难,而当你真正的理解了一件事情的意图,就能很容易的从各个方面入手了。

二、单元测试的价值

减少低级错误

这一点是毋庸质疑的,测试所存在的最主要价值就是帮我们解决错误,单元测试也是这样。当我们在对自己的代码进行测试时,能很容易的就排除掉一些非常低级的错误,起码我们能够保证,在一些正常的情况下,代码是可以正常工作的。

当经历的语言和平台越来越多,很多平台相关的特性有时候并不是靠感觉就能拿得准的,比如你并不清楚NSString对象的equalTo和equalToString这两个方法执行效果是否相同,那么你就有必要对使用到的代码进行测试去验证下,避免出现人为意识造成的低级Bug。

减少调试时间

可以说,在开发中我们有大部分的时间可能都是出于调试状态,减少调试时间,自然也就提高了产出率,而单元测试是否能提高产出率一直也是有点争议,不过它的确能够有效的减少调试时间。

在一个应用中,并不是所有需要调试的代码都在程序的入口点,所以,当我们需要调试时,会花费一些额外的时间来触发调试的代码。单元测试就能很好的解决这个问题,我们针对需要调试的代码,构建相关测试上下文,配合IDE,能方便快速的进行反复模拟、测试。

描述代码行为

很多书上都会说,代码就是最好的文档(当然是写得比较好的代码),注释需要能够精简,否则大片的注释会影响阅读。这点我是非常赞同的,而单元测试,作为代码的一等公民,我觉得它能更好的描述代码的行为。在撰写单元测试时,我们基本上都是假定某个方法,在某个特定的环境中,能够有预期的表现。如果这样的测试足够完善,那么,当我们去看别人测试时,就能很清楚他提供的方法是为了适应怎样的场景,能够更好的理解设计者的意图。

可维护性增强

当一个项目中单元测试的覆盖率很可观,后期在对代码进行修改时,能够很容易就知道是否破坏了老的业务逻辑,这样大大的降低了回归出错的可能性。当我们从测试那获得一个Bug时,可以通过测试用例去还原,当我们这个测试通过后,这个Bug也就解决了,而这个Bug Fix的测试用例也保证了以后这个Bug不会再次复现。

这会是一个很好的良性循环,我们的代码会越来越健壮,而我们可以把心思放在更多更有意义的事情上,比如重构。有了单元测试的保障,我们可以比较大胆的进行重构设计,当然,在重构时单元测试也会成为一种负担,我们可能需要同时重构单元测试,不过,相比于可靠性,这种负担还是非常值得去承受的。

改善设计

测试驱动设计,这在敏捷开发中是非常火热的名词,但我自身并不认为在一个较大型的项目中,能够完全按照这样的方式来驱动。虽然如此,但测试从一定的程度上能够改善设计,比如为了让一些类的某些行为中的细节得到充分测试(心里不再惴惴不安),我们就必须要对这些行为进行细分,于是我们开始提取方法,构建测试用例。这样,我们方法的行为会越来越单一,而良好的类设计中,正是需要这样的方法设计。

三、测试用例三部曲

如何比较好的来编写一个测试用例,对此,有很多不同的做法,而这也并没有一个标准,也不需要有一个标准。我们需要清楚一个测试用例存在的意义是什么,它是为了验证某个类的某个行为在某种上下文中能得到预期的结果,如果你的测试用例达到了这样的目的,那么如何写也都不算错。不过,为了能够统一单元测试的规范(这点在多人协同开发下非常重要),我们常常会把一个测试用例分为三个阶段:排列资源、执行行为、断言结果,一般我会习惯用Arrange、Act、Assert来表示,也会有用Given,When,Then来表示的,但意思都相同。

排列资源

排列资源,便是提供一切测试方法所需要的东西,而这些东西便称之为资源。这些资源包括:

  • 1.方法输入的参数
  • 2.方法所执行的特定上下文

这个阶段相当于准备阶段,一切都是为了这个用例中执行行为而准备,如果没有任何需要准备的数据,这个阶段是可以被忽略的。

这里我们以测试 NSMutableDictionarydic setObject: forKey: 为例,那么在排列资源阶段,我们的代码如下:

- (void)test_setObject$forKey {
    // arrange
    NSString *key = @"test_key";
    NSString *value = @"test_value";
    NSMutableDictionary *dic = [NSMutableDictionary new];
}

关于测试用例的命名,比较推崇这样的写法:

test_测试方法签名_测试上下文

由于Objective-C的方法签名比较奇怪,为了可读性,建议使用$进行分割,比如这个示例中的test_setObject$forKey,或者附带上下文的test_setObject$forKey_when_key_is_nil。

执行行为

当准备阶段完毕后,便进入要测试行为的执行阶段,在这个阶段,我们会使用准备好的资源,并记录下行为输出以供下个阶段使用。这里的行为输出不一定就是方法执行的返回值,很多时候我们要测试的方法并没有任何返回值,但一个方法执行后,总归有一个预期的行为发生,即便是空方法也是(什么也不会改变),而这个行为预期便是测试行为的输出。

加入执行行为的代码:

- (void)test_setObject$forKey {
    // arrange
    NSString *key = @"test_key";
    NSString *value = @"test_value";
    NSMutableDictionary *dic = [NSMutableDictionary new];

    // act
    [dic setObject:value forKey:key];
}
断言结果

最后一步,也是核心的一步,它决定着一个 测试用例的成功与否,我们需要在这一步断言执行行为的输出是否达到预期。确定一个行为的输出,我们可能需要多次断言,这里需要遵循一个原则:** 先执行的断言,不应该以以后断言的成功为前提。** 以上原则很重要,这对快速排除Bug会很有帮助。现在,我们来看下针对 NSMutableDictionary 的这个完整测试用例:


- (void)test_setObject$forKey {
    // arrange
    NSString *key = @"test_key";
    NSString *value = @"test_value";
    NSMutableDictionary *dic = [NSMutableDictionary new];

    // act
    [dic setObject:value forKey:key];

    // assert
    XCTAssertNotNil([dic objectForKey:key]);
    XCTAssertEqual([dic objectForKey:key], value);
}

可以看到,最后我们先断言是否为空,再断言是否相等,后者是在前者成功的前提下才可能不失败。如果颠倒顺序,就很难尽早的发现错误原因,我们应该下意识的将这种断言的依赖关系排序正确,就像我们在很多语言里使用 try...catch 时,我们会排列好异常捕获的顺序。

四、做到真正的单元测试

不知道大家有没有认真想过,这种测试为什么要叫Unit Test?顾名思义,是针对Unit来进行测试,也就是针对基本的单元进行测试。所以要做到真正的单元测试,你需要保证你每个测试用例所针对的仅仅是一个基本的单元,而不是很多复杂依赖的综合行为。

关于行为测试

在面向对象的程序设计中,一般最基本的单元就是一个类的方法,所以在单元测试中,我们要面对的就是针对这些方法编写合适的测试用例。方法就是一个类的对外行为,针对方法的测试也可以看做是针对一个类的行为测试,在编写测试用例时,我们不应该考虑一个行为的中间产出,我们应该将关注点放在最终的测试执行结果上。

关于行为测试,目前已经有一套相关的理论和相应的测试框架,可以参考 行为驱动开发

关于隔离依赖

前面也提到了,我们要的是针对一个基本单元的测试,这样的要求会促使我们改善设计。我们应该竟可能让类方法的职责单一,这会方便我们撰写测试用例。理想中,每个类都是独立的,但现实里,一个类很少会没有依赖关系,而在编写测试用例时,我们不应该将依赖的类行为纳入到该类的测试用例中,被依赖的类应该是经过了单独测试,我们需要假定它是完全合理正确的。

为了能够不受依赖类的实例影响,我们可以将依赖的行为抽象成接口,依赖类去实现这样一个接口,最终可以通过构造函数或者其他方式注入进来。我们通过单元测试,又将设计推导到了另一个高度:依赖抽象而不是实现具体细节。 通过接口隔离依赖后,在单元测试里,我们可以撰写一些用于测试的模拟实现,也就是我们实现这样一个接口,但只是为了测试某种行为去实现它,这便是所谓的 ** Mock**。

手动实现一个个Mock是非常耗时的,为了测试不同的行为,我们可能需要不同的Mock对象,幸好几乎每一平台的单元测试都会有相应得Mock框架,Objective-C也不例外,推荐使用 OCMockito ,官方示例也很有代表性:

// mock creation
NSMutableArray *mockArray = mock([NSMutableArray class]);

// using mock object
[mockArray addObject:@"one"];
[mockArray removeAllObjects];

// verification
[verify(mockArray) addObject:@"one"];
[verify(mockArray) removeAllObjects];

虽然这个Mock框架可以构建Class级别的模拟抽象,但,我们应该把这种Class当做是其它语义平台中的抽象类。前面说过了,我们应该尽可能的依赖于抽象,而不是实现细节。

五、接口模拟与集成测试

为什么我们需要通过模拟去测试类的行为?既然这个类有依赖,何不将他依赖的具体实现直接使用在测试用例里?这样单元测试和运行时效果还会更加接近。

相信很多人都有过上面这样的疑问,其实根本原因还是很简单的:关注点更单一。怎样才能做好一件事情,那就要足够的专注,任何所谓的成功都离不开专注。单元测试专注于一个单元的测试,它不是多个单元糅合在一起,这样才能保证变化点都集中在被测试的单元中,才能体现出更好的维护价值。
那么,当我们几乎将所有类的公开行为都进行了单元测试,这时候我们就应该去编写集成测试了,集成测试与单元测试的关注点不同,它关心的是实现类在特定场景下交互的最终结果,可以说集成测试会更加动态,它可以模拟很多业务场景,而单元测试相对比较静态,它只是用来验证一个动作的正确性。

所以,在优良的测试项目中,单元测试会和集成测试分开,当然现实中不一定会这么做。就比如我们测试 REST API 时,单元测试应该回去模拟网络返回的数据,而集成测试才会真实的发送网络请求,很多时候我们都直接使用了后者,这样做感觉很方便,而好坏就留给大家自己去斟酌吧。

六、总而言之

经过漫长的岁月洗礼,你终会变成一个热爱它的猿。单元测试的利弊需要你在不同的项目中反复斟酌,任何一门技术都是需要不断总结,从而能向更高层次演化。从现在开始,让单元测试来帮你描述代码行为,并保证它的健壮性,而不是人为去规避一些设计缺陷。

本章并没有提供很多实际场景的测试方式,但理解了这件事情的动机后,便可以自己去处理各种细枝末节。任何测试方式,它们的中心思想也是万变不离其宗,只是手段不同罢了。授人以鱼不如授人以渔,有了良好的基础思想,我相信通过强大的搜索引擎,我们一定也可以在这个领域里找到一份属于自己的归属感。

单元测试时可能遇到的坑:找不到文件

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

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,580评论 18 139
  • 摘自http://www.51testing.com/html/75/n-3721875.html 单元测试,一个...
    许小小晴阅读 402评论 0 1
  • 文章来自:http://blog.csdn.net/mj813/article/details/52451355 ...
    好大一只鹏阅读 9,186评论 2 126
  • 单元测试不是一个小工程,需要多用些时间才能做好,不要希望通过这个文章就能掌握单元测试,这只是一个入门,需要自己动手...
    勇不言弃92阅读 7,776评论 9 60
  • Android单元测试介绍 处于高速迭代开发中的Android项目往往需要除黑盒测试外更加可靠的质量保障,这正是单...
    东经315度阅读 3,086评论 6 37