TDD的iOS开发初步以及Kiwi使用入门

测试驱动开发(Test Driven Development,如下简称TDD)是保证代码质量的不二法则,也是先进程序开发的共识。Apple一直致力于在iOS开发中集成更加方便和可用的测试,在Xcode 5中,新的IDE和SDK引入了XCTest来替代原来的SenTestingKit,而且取消了新建工程时的“包括单元测试”的可选项(一样待遇的还有使用ARC的可选项)。新工程将自动包含测试的target,而且相关框架也搭建完毕,能够说测试终于摆脱了iOS开发中“二等公民”的地位,如今已经变得和产品代码同样重要了。我相信每一个工程师在完成本身的业务代码的同时,也有最基本的编写和维护相应的测试代码的义务,以保证本身的代码可以正确运行。更进一步,若是可以使用TDD来进行开发,不只能保证代码运行的正确性,也有助于代码结构的安排和思考,有助于自身的不断提升。我在最开始进行开发时也曾对测试嗤之以鼻,但后来无数的惨痛教训让我明白那么多工程师痴迷于测试或者追求更完美的测试,是有其深入含义的。若是您以前尚未开始为您的代码编写测试,我强烈建议,从今天开始,从如今开始(也许作不到的话,也请从下一个项目开始),编写测试,或者尝试一下TDD的开发方式。ios

Kiwi是一个iOS平台十分好用的行为驱动开发(Behavior Driven Development,如下简称BDD)的测试框架,有着很是漂亮的语法,能够写出结构性强,很是容易读懂的测试。由于国内如今有关Kiwi的介绍比较少,加上在测试这块很能不少工程师们并无特别留意,水平层次可能相差会很远,所以在这一系列的两篇博文中,我将从头开始先简单地介绍一些TDD的概念和思想,而后从XCTest的最简单的例子开始,过渡到Kiwi的测试世界。在下一篇中我将继续深刻介绍一些Kiwi的其余稍高一些的特性,以期更多的开发者可以接触并使用Kiwi这个优秀的测试框架。git

什么是TDD,为何咱们要TDD

测试驱动开发并非一个很新鲜的概念了。软件开发工程师们(固然包括你我)最开始学习程序编写时,最喜欢干的事情就是编写一段代码,而后运行观察结果是否正确。若是不对就返回代码检查错误,或者是加入断点或者输出跟踪程序并找出错误,而后再次运行查看输出是否与预想一致。若是输出只是控制台的一个简单的数字或者字符那还好,可是若是输出必须在点击一系列按钮以后才能在屏幕上显示出来的东西呢?难道咱们就只能一次一次地等待编译部署,启动程序而后操做UI,一直点到咱们须要观察的地方么?这种行为无疑是对美好生命和绚丽青春的巨大浪费。因而有一些已经浪费了无数时间的资深工程师们忽然发现,原来咱们能够在代码中构建出一个相似的场景,而后在代码中调用咱们以前想检查的代码,并将运行的结果与咱们的设想结果在程序中进行比较,若是一致,则说明了咱们的代码没有问题,是按照预期工做的。好比咱们想要实现一个加法函数add,输入两个数字,输出它们相加后的结果。那么咱们不妨设想咱们真的拥有两个数,好比3和5,根据人人会的十之内的加法知识,咱们知道答案是8.因而咱们在相加后与预测的8进行比较,若是相等,则说明咱们的函数实现至少对于这个例子是没有问题的,所以咱们对“这个方法能正确工做”这一命题的信心就增长了。这个例子的伪码以下:github

//Product Code add(float num1, float num 2) {...} //Test code let a = 3; let b = 5; let c = a + b; if (c == 8) { // Yeah, it works! } else { //Something wrong! } 

当测试足够全面和具备表明性的时候,咱们即可以信心爆棚,拍着胸脯说,这段代码没问题。咱们作出某些条件和假设,并以其为条件使用到被测试代码中,并比较预期的结果和实际运行的结果是否相等,这就是软件开发中测试的基本方式。数组

为何咱们要test

而TDD是一种相对于普通思惟的方式来讲,比较极端的一种作法。咱们通常能想到的是先编写业务代码,也就是上面例子中的add方法,而后为其编写测试代码,用来验证产品方法是否是按照设计工做。而TDD的思想正好与之相反,在TDD的世界中,咱们应该首先根据需求或者接口状况编写测试,而后再根据测试来编写业务代码,而这实际上是违反传统软件开发中的先验认知的。可是咱们能够举一个生活中相似的例子来讲明TDD的必要性:有经验的砌砖师傅老是会先拉一条垂线,而后沿着线砌砖,由于有直线的保证,所以能够作到笔直整齐;而新入行的师傅每每二话不说直接开工,而后在一阶段完成后再用直尺垂线之类的工具进行测量和修补。TDD的好处不言自明,由于老是先测试,再编码,因此至少你的全部代码的public部分都应该含有必要的测试。另外,由于测试代码实际是要使用产品代码的,所以在编写产品代码前你将有一次深刻思考和实践如何使用这些代码的机会,这对提升设计和可扩展性有很好的帮助,试想一下你测试都很难写的接口,别人(或者本身)用起来得多纠结。在测试的准绳下,你能够有目的有方向地编码;另外,由于有测试的保护,你能够放心对原有代码进行重构,而没必要担忧破坏逻辑。这些其实都指向了一个最终的目的:让咱们快乐安心高效地工做。安全

在TDD原则的指导下,咱们先编写测试代码。这时由于尚未对应的产品代码,因此测试代码确定是没法经过的。在大多数测试系统中,咱们使用红色来表示错误,所以一个测试的初始状态应该是红色的。接下来咱们须要使用最小的代价(最少的代码)来让测试经过。经过的测试将被表示为安全的绿色,因而咱们回到了绿色的状态。接下来咱们能够添加一些测试例,来验证咱们的产品代码的实现是否正确。若是不幸新的测试例让咱们回到了红色状态,那咱们就能够修改产品代码,使其回到绿色。如此反复直到各类边界和测试都进行完毕,此时咱们即可以获得一个具备测试保证,鲁棒性超强的产品代码。在咱们以后的开发中,由于你有这些测试的保证,你能够大胆重构这段代码或者与之相关的代码,最后只须要保证项目处于绿灯状态,你就能够保证代码没重构没有出现问题。ruby

简单说来,TDD的基本步骤就是“红→绿→大胆重构”。bash

使用XCTest来执行TDD

Xcode 5中已经集成了XCTest的测试框架(以前版本是SenTestingKit和OCUnit),所谓测试框架,就是一组让“将测试集成到工程中”以及“编写和实践测试”变得简单的库。咱们以后将经过实现一个栈数据结构的例子,来用XCTest初步实践一下TDD开发。在你们对TDD有一些直观认识以后,再转到Kiwi的介绍。若是您已经在使用XCTest或者其余的测试框架了的话,能够直接跳过本节。数据结构

首先咱们用Xcode新建一个工程吧,选择模板为空项目,在Product Name中输入工程名字VVStack,固然您可使用本身喜欢的名字。若是您使用过Xcode以前的版本的话,应该有留意到以前在这个界面是能够选择是否使用Unit Test的,可是如今这个选框已经被取消。框架

新建工程

新建工程后,能够发如今工程中默认已经有一个叫作VVStackTests的target了,这就是咱们测试时使用的target。测试部分的代码默认放到了{ProjectName}Tests的group中,如今这个group下有一个测试文件VVStackTests.m。咱们的测试例不须要向别的类暴露接口,所以不须要.h文件。另一般XCTest的测试文件都会以Tests来作文件名结尾。异步

Test文件和target

运行测试的快捷键是⌘U(或者可使用菜单的Product→Test),咱们这时候直接对这个空工程进行测试,Xcode在编译项目后会使用你选择的设备或者模拟器运行测试代码。不出意外的话,此次测试将会失败,如图:

失败的初始测试

VVStackTests.m是Xcode在新建工程时自动为咱们添加的测试文件。由于这个文件并不长,因此咱们能够将其内容所有抄录以下:

#import <XCTest/XCTest.h> @interface VVStackTests : XCTestCase @end @implementation VVStackTests - (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 { XCTFail(@"No implementation for \"%s\"", __PRETTY_FUNCTION__); } @end 

能够看到,VVStackTestsXCTestCase的子类,而XCTestCase正是XCTest测试框架中的测试用例类。XCTest在进行测试时将会寻找测试target中的全部XCTestCase子类,并运行其中以test开头的全部实例方法。在这里,默认实现的-testExample将被执行,而在这个方法里,Xcode默认写了一个XCTFail的断言,来强制这个测试失败,用以提醒咱们测试尚未实现。所谓断言,就是判断输入的条件是否知足。若是不知足,则抛出错误并输出预先规定的字符串做为提示。在这个Fail的断言必定会失败,并提示没有实现该测试。另外,默认还有两个方法-setUp-tearDown,正如它们的注释里所述,这两个方法会分别在每一个测试开始和结束的时候被调用。咱们如今正要开始编写咱们的测试,因此先将原来的-testExample删除掉。如今再使用⌘U来进行测试,应该能够顺利经过了(由于咱们已经没有任何测试了)。

接下来让咱们想一想要作什么吧。咱们要实现一个简单的栈数据结构,那么固然会有一个类来表明这种数据结构,在这个工程中我打算就叫它VVStack。按照常规,咱们能够新建一个Cocoa Touch类,继承NSObject而且开始实现了。可是别忘了,咱们如今在TDD,咱们须要先写测试!那么首先测试的目标是什么呢?没错,是测试这个VVStack类是否存在,以及是否可以初始化。有了这个目标,咱们就能够动手开始编写测试了。在文件开头加上#import "VVStack.h",而后在VVStackTests.m@end前面加上以下代码:

- (void)testStackExist { XCTAssertNotNil([VVStack class], @"VVStack class should exist."); } - (void)testStackObjectCanBeCreated { VVStack *stack = [VVStack new]; XCTAssertNotNil(stack, @"VVStack object can be created."); } 

嘛,固然是不可能经过测试的,并且甚至连编译都没法完成,由于咱们如今根本没有一个叫作VVStack的类。最简单的让测试经过的方法就是在产品代码中添加VVStack类。新建一个Cocoa Touch的Objective-C class,取名VVStack,做为NSObject的子类。注意在添加的时候,应该只将其加入产品的target中:

添加类的时候注意选择合适的target

因为VVStack是NSObject的子类,因此上面的两个断言应该都能经过。这时候再运行测试,成功变绿。接下来咱们开始考虑这个类的功能:栈的话确定须要可以push,而且push后的栈顶元素应该就是刚才所push进去的元素。那么创建一个push方法的测试吧,在刚才添加的代码之下继续写:

- (void)testPushANumberAndGetIt { VVStack *stack = [VVStack new]; [stack push:2.3]; double topNumber = [stack top]; XCTAssertEqual(topNumber, 2.3, @"VVStack should can be pushed and has that top value."); } 

由于咱们尚未实现-push:-top方法,因此测试毫无疑问地失败了(在ARC环境中直接没法编译)。为了使测试当即经过咱们首先须要在VVStack.h中声明这两个方法,而后在.m的实现文件中进行实现。令测试经过的最简单的实现是一个空的push方法以及直接返回2.3这个数:

//VVStack.h @interface VVStack : NSObject - (void)push:(double)num; - (double)top; @end //VVStack.m @implementation VVStack - (void)push:(double)num { } - (double)top { return 2.3; } @end 

再次运行测试,咱们顺利回到了绿灯状态。也许你很快就会说,这算哪门子实现啊,若是再增长一组测试例,好比push一个4.6,而后检查top,不就失败了么?咱们难道不该该直接实现一个真正的合理的实现么?对此的回答是,在实际开发中,咱们确定不会以这样的步伐来处理像例子中这样相似的简单问题,而是会直接跳过一些error-try的步骤,实现一个比较完整的方案。可是在更多的时候,咱们所关心和须要实现的目标并非这样容易。特别是在对TDD还不熟悉的时候,咱们有必要放慢节奏和动做,将整个开发理念进行充分实践,这样才有可能在以后更复杂的案例中正确使用。因而咱们发扬不怕繁杂,精益求精的精神,在刚才的测试例上增长一个测试,回到VVStackTests.m中,在刚才的测试方法中加上:

- (void)testPushANumberAndGetIt { //... [stack push:4.6]; topNumber = [stack top]; XCTAssertEqual(topNumber, 4.6, @"Top value of VVStack should be the last num pushed into it"); } 

很好,这下子咱们回到了红灯状态,这正是咱们所指望的,如今是时候来考虑实现这个栈了。这个实现过于简单,也有很是多的思路,其中一种是使用一个NSMutableArray来存储数据,而后在top方法里返回最后加入的数据。修改VVStack.m,加入数组,更改实现:

//VVStack.m @interface VVStack() @property (nonatomic, strong) NSMutableArray *numbers; @end @implementation VVStack - (id)init { if (self = [super init]) { _numbers = [NSMutableArray new]; } return self; } - (void)push:(double)num { [self.numbers addObject:@(num)]; } - (double)top { return [[self.numbers lastObject] doubleValue]; } @end 

测试经过,注意到在-testStackObjectCanBeCreatedtestPushANumberAndGetIt两个测试中都生成了一个VVStack对象。在这个测试文件中基本每一个测试都会须要初始化对象,所以咱们能够考虑在测试文件中添加一个VVStack的实例成员,并将测试中的初始化代码移到-setUp中,并在-tearDown中释放。

接下来咱们能够模仿继续实现pop等栈的方法。鉴于篇幅这里再也不继续详细实现,你们能够本身动手试试看。记住先实现测试,而后再实现产品代码。一开始您可能会以为这很无聊,效率低下,可是请记住这是起步练习不可缺乏的一部分,并且在咱们的例子中其实一切都是以“慢动做”在进行的。相信在通过实践和使用后,您将会逐渐掌握本身的节奏和重点测试。关于使用XCTest到这里为止的代码,能够在github上找到。

Kiwi和BDD的测试思想

XCTest是基于OCUnit的传统测试框架,在书写性和可读性上都不太好。在测试用例太多的时候,因为各个测试方法是割裂的,想在某个很长的测试文件中找到特定的某个测试并搞明白这个测试是在作什么并非很容易的事情。全部的测试都是由断言完成的,而不少时候断言的意义并非特别的明确,对于项目交付或者新的开发人员加入时,每每要花上很大成原本进行理解或者转换。另外,每个测试的描述都被写在断言以后,夹杂在代码之中,难以寻找。使用XCTest测试另一个问题是难以进行mock或者stub,而这在测试中是很是重要的一部分(关于mock测试的问题,我会在下一篇中继续深刻)。

行为驱动开发(BDD)正是为了解决上述问题而生的,做为第二代敏捷方法,BDD提倡的是经过将测试语句转换为相似天然语言的描述,开发人员可使用更符合大众语言的习惯来书写测试,这样不论在项目交接/交付,或者以后本身修改时,均可以顺利不少。若是说做为开发者的咱们平常工做是写代码,那么BDD其实就是在讲故事。一个典型的BDD的测试用例包活完整的三段式上下文,测试大多能够翻译为Given..When..Then的格式,读起来轻松惬意。BDD在其余语言中也已经有一些框架,包括最先的Java的JBehave和赫赫有名的Ruby的RSpecCucumber。而在objc社区中BDD框架也正在欣欣向荣地发展,得益于objc的语法原本就很是接近天然语言,再加上C语言宏的威力,咱们是有可能写出漂亮优美的测试的。在objc中,如今比较流行的BDD框架有cedarspectaKiwi。其中我的比较喜欢Kiwi,使用Kiwi写出的测试看起来大概会是这个样子的:

describe(@"Team", ^{ context(@"when newly created", ^{ it(@"should have a name", ^{ id team = [Team team]; [[team.name should] equal:@"Black Hawks"]; }); it(@"should have 11 players", ^{ id team = [Team team]; [[[team should] have:11] players]; }); }); }); 

咱们很容易根据上下文将其提取为Given..When..Then的三段式天然语言

Given a team, when newly created, it should have a name, and should have 11 players

很简单啊有木有!在这样的语法下,是否是写测试的兴趣都被激发出来了呢。关于Kiwi的进一步语法和使用,咱们稍后详细展开。首先来看看如何在项目中添加Kiwi框架吧。

在项目中添加Kiwi

最简单和最推荐的方法固然是CocoaPods,若是您对CocoaPods还比较陌生的话,推荐您花时间先看一看这篇CocoaPods的简介。Xcode 5和XCTest环境下,咱们须要在Podfile中添加相似下面的条目(记得将VVStackTests换成您本身的项目的测试target的名字):

target :VVStackTests, :exclusive => true do pod 'Kiwi/XCTest' end 

以后pod install之后,打开生成的xcworkspace文件,Kiwi就已经处于可用状态了。另外,为了咱们在新建测试的时候能省点事儿,能够在官方repo里下载并运行安装Kiwi的Xcode Template。若是您坚持不用CocoaPods,而想要本身进行配置Kiwi的话,能够参考这篇wiki

行为描述(Specs)和指望(Expectations),Kiwi测试的基本结构

咱们先来新建一个Kiwi测试吧。若是安装了Kiwi的Template的话,在新建文件中选择Kiwi/Kiwi Spec来创建一个Specs,取名为SimpleString,注意选择目标target为咱们的测试target,模板将会在新建的文件名字后面加上Spec后缀。传统测试的文件名通常以Tests为后缀,表示这个文件中含有一组测试,而在Kiwi中,一个测试文件所包含的是一组对于行为的描述(Spec),所以习惯上使用须要测试的目标类来做为名字,并以Spec做为文件名后缀。在Xcode 5中创建测试时已经不会同时建立.h文件了,可是如今的模板中包含有对同名.h的引用,能够在建立后将其删去。若是您没有安装Kiwi的Template的话,能够直接建立一个普通的Objective-C test case class,而后将内容替换为下面这样:

#import <Kiwi/Kiwi.h> SPEC_BEGIN(SimpleStringSpec) describe(@"SimpleString", ^{ }); SPEC_END 

你可能会以为这不是objc代码,甚至怀疑这些语法是否可以编译经过。其实SPEC_BEGINSPEC_END都是宏,它们定义了一个KWSpec的子类,并将其中的内容包装在一个函数中(有兴趣的朋友不妨点进去看看)。咱们如今先添加一些描述和测试语句,并运行看看吧,将上面的代码的SPEC_BEGINSPEC_END之间的内容替换为:

describe(@"SimpleString", ^{ context(@"when assigned to 'Hello world'", ^{ NSString *greeting = @"Hello world"; it(@"should exist", ^{ [[greeting shouldNot] beNil]; }); it(@"should equal to 'Hello world'", ^{ [[greeting should] equal:@"Hello world"]; }); }); }); 

describe描述须要测试的对象内容,也即咱们三段式中的Givencontext描述测试上下文,也就是这个测试在When来进行,最后it中的是测试的本体,描述了这个测试应该知足的条件,三者共同构成了Kiwi测试中的行为描述。它们是能够nest的,也就是一个Spec文件中能够包含多个describe(虽然咱们不多这么作,一个测试文件应该专一于测试一个类);一个describe能够包含多个context,来描述类在不一样情景下的行为;一个context能够包含多个it的测试例。让咱们运行一下这个测试,观察输出:

VVStack[36517:70b] + 'SimpleString, when assigned to 'Hello world', should exist' [PASSED] VVStack[36517:70b] + 'SimpleString, when assigned to 'Hello world', should equal to 'Hello world'' [PASSED] 

能够看到,这三个关键字的描述将在测试时被依次打印出来,造成一个完整的行为描述。除了这三个以外,Kiwi还有一些其余的行为描述关键字,其中比较重要的包括

  • beforeAll(aBlock) - 当前scope内部的全部的其余block运行以前调用一次
  • afterAll(aBlock) - 当前scope内部的全部的其余block运行以后调用一次
  • beforeEach(aBlock) - 在scope内的每一个it以前调用一次,对于context的配置代码应该写在这里
  • afterEach(aBlock) - 在scope内的每一个it以后调用一次,用于清理测试后的代码
  • specify(aBlock) - 能够在里面直接书写不须要描述的测试
  • pending(aString, aBlock) - 只打印一条log信息,不作测试。这个语句会给出一条警告,能够做为一开始集中书写行为描述时还未实现的测试的提示。
  • xit(aString, aBlock) - 和pending同样,另外一种写法。由于在真正实现时测试时只须要将x删掉就是it,可是pending语意更明确,所以仍是推荐pending

能够看到,因为有context的存在,以及其能够嵌套的特性,测试的流程控制相比传统测试能够更加精确。咱们更容易把before和after的做用区域限制在合适的地方。

实际的测试写在it里,是由一个一个的指望(Expectations)来进行描述的,指望至关于传统测试中的断言,要是运行的结果不能匹配指望,则测试失败。在Kiwi中指望都由should或者shouldNot开头,并紧接一个或多个判断的的链式调用,大部分常见的是be或者haveSomeCondition的形式。在咱们上面的例子中咱们使用了should not be nil和should equal两个指望来确保字符串赋值的行为正确。其余的指望语句很是丰富,而且都符合天然语言描述,因此并不须要太多介绍。在使用的时候不妨直接按照本身的想法来描述本身的指望,通常状况下在IDE的帮助下咱们都能找到想要的结果。若是您想看看完整的指望语句的列表,能够参看文档的这个页面。另外,您还能够经过新建KWMatcher的子类,来简单地自定义本身和项目所须要的指望语句。从这一点来看,Kiwi能够说是一个很是灵活并具备可扩展性的测试框架。

到此为止的代码能够从这里找到。

Kiwi实际使用实例

最后咱们来用Kiwi完整地实现VVStack类的测试和开发吧。首先重写刚才XCTest的相关测试:新建一个VVStackSpec做为Kiwi版的测试用例,而后把describe换成下面的代码:

describe(@"VVStack", ^{ context(@"when created", ^{ __block VVStack *stack = nil; beforeEach(^{ stack = [VVStack new]; }); afterEach(^{ stack = nil; }); it(@"should have the class VVStack", ^{ [[[VVStack class] shouldNot] beNil]; }); it(@"should exist", ^{ [[stack shouldNot] beNil]; }); it(@"should be able to push and get top", ^{ [stack push:2.3]; [[theValue([stack top]) should] equal:theValue(2.3)]; [stack push:4.6]; [[theValue([stack top]) should] equal:4.6 withDelta:0.001]; }); }); }); 

看到这里的您看这段测试应该不成问题。须要注意的有两点:首先stack分别是在beforeEachafterEach的block中的赋值的,所以咱们须要在声明时在其前面加上__block标志。其次,指望描述的should或者shouldNot是做用在对象上的宏,所以对于标量,咱们须要先将其转换为对象。Kiwi为咱们提供了一个标量转对象的语法糖,叫作theValue,在作精确比较的时候咱们能够直接使用例子中直接与2.3作比较这样的写法来进行对比。可是若是测试涉及到运算的话,因为浮点数精度问题,咱们通常使用带有精度的比较指望来进行描述,即4.6例子中的equal:withDelta:(固然,这里只是为了demo,实际在这用和上面2.3同样的方法就行了)。

接下来咱们再为这个context添加一个测试例,用来测试初始情况时栈是否为空。由于咱们使用了一个Array来做为存储容器,根据咱们以前用过的equal方法,咱们很容易想到下面这样的测试代码

it(@"should equal contains 0 element", ^{ [[theValue([stack.numbers count]) should] equal:theValue(0)]; }); 

这段测试在逻辑上没有太大问题,可是有很是多值得改进的地方。首先若是咱们须要将原来写在Extension里的numbers暴露到头文件中,这对于类的封装是一种破坏,对于这个,一种常见的作法是只暴露一个-count方法,让其返回numbers的元素个数,从而保证numbers的私有性。另外对于取值和转换,其实theValue的存在在必定程度上是破坏了测试可读性的,咱们能够想办法改善一下,好比对于0的来讲,咱们有beZero这样的指望可使用。简单改写之后,这个VVStack.h和这个测试能够变成这个样子:

//VVStack.h //... - (NSUInteger)count; //... //VVStack.m //... - (NSUInteger)count { return [self.numbers count]; } //... it(@"should equal contains 0 element", ^{ [[theValue([stack count]) should] beZero]; }); 

更进一步地,对于一个collection来讲,Kiwi有一些特殊处理,好比havehaveCountOf系列的指望。若是测试的对象实现了-count方法的话,咱们就可使用这一系列指望来写出更好的测试语句。好比上面的测试还能够进一步写成

it(@"should equal contains 0 element", ^{ [[stack should] haveCountOf:0]; }); 

在这种状况下,咱们并无显式地调用VVStack的-count方法,因此咱们能够在头文件中将其删掉。可是咱们须要保留这个方法的实现,由于测试时是须要这个方法的。若是测试对象不能响应count方法的话,如你所料,测试时会扔一个unrecognized selector的错。Kiwi的内部实现是一个大量依赖了一个个行为Matcher和objc的消息转发,对objcruntime特性比较熟悉,并想更深刻的朋友不放能够看看Kiwi的源码,写得至关漂亮。

其实对于这个测试,咱们还能够写出更漂亮的版本,像这样:

it(@"should equal contains 0 element", ^{ [[stack should] beEmpty]; }); 

好了。关于空栈这个情景下的测试感受差很少了。咱们继续用TDD的思想来完善VVStack类吧。栈的话,咱们固然须要可以-pop,也就是说在(Given)给定一个栈时,(When)当栈中有元素的时候,(Then)咱们能够pop它,而且获得栈顶元素。咱们新建一个context,而后按照这个思路书写行为描述(测试):

context(@"when new created and pushed 4.6", ^{ __block VVStack *stack = nil; beforeEach(^{ stack = [VVStack new]; [stack push:4.6]; }); afterEach(^{ stack = nil; }); it(@"can be poped and the value equals 4.6", ^{ [[theValue([stack pop]) should] equal:theValue(4.6)]; }); it(@"should contains 0 element after pop", ^{ [stack pop]; [[stack should] beEmpty]; }); }); 

完成了测试书写后,咱们开始按照设计填写产品代码。在VVStack.h中完成申明,并在.m中加入相应实现。

- (double)pop { double result = [self top]; [self.numbers removeLastObject]; return result; } 

很简单吧。并且由于有测试的保证,咱们在提供像Stack这样的基础类时,就不须要等到或者在真实的环境中检测了。由于在被别人使用以前,咱们本身的测试代码已经可以保证它的正确性了。VVStack剩余的最后一个小问题是,在栈是空的时候,咱们执行pop操做时应该给出一个错误,用以提示空栈没法pop。虽然在objc中异常并不常见,可是在这个情景下是抛异常的好时机,也符合通常C语言对于出空栈的行为。咱们能够在以前的“when created”上下文中加入一个指望:

it(@"should raise a exception when pop", ^{ [[theBlock(^{ [stack pop]; }) should] raiseWithName:@"VVStackPopEmptyException"]; }); 

theValue配合标量值相似,theBlock也是Kiwi中的一个转换语法,用来将一段程序转换为相应的matcher,使其能够被施加指望。这里咱们指望空的Stack在被pop时抛出一个叫作"VVStackPopEmptyException"的异常。咱们能够重构pop方法,在栈为空时给一个异常:

- (double)pop { if ([self count] == 0) { [NSException raise:@"VVStackPopEmptyException" format:@"Can not pop an empty stack."]; } double result = [self top]; [self.numbers removeLastObject]; return result; } 

进一步的Kiwi

VVStack的测试和实现就到这里吧,根据这套测试,您可使用本身的实现来轻易地重构这个类,而没必要担忧破坏它的公共接口的行为。若是须要添加新的功能或者修正已有bug的时候,咱们也能够经过添加或者修改相应的测试,来确保正确性。我将会在下一篇博文中继续介绍Kiwi,看看Kiwi在异步测试和mock/stub的使用和表现如何。Kiwi如今还在比较快速的发展中,官方repo的wiki上有一些不错的资料和文档,能够参考。VVStack的项目代码能够在这个repo上找到,能够做为参考。

相关文章
相关标签/搜索