TDD的iOS开发初步以及Kiwi使用入门
代码不方便阅读:查看底部“阅读原文”即可 测试驱动开发(Test Driven Development,以下简称TDD)是保证代码质量的不二法则,也是先进程序开发的共识。Apple一直致力于在iOS开发中集成更加方便和可用的 测试,在Xcode 5中,新的IDE和SDK引入了XCTest来替代原来的SenTestingKit,并且取消了新建工程时的“包括单元测试”的可选项(同样待遇的还有 使用ARC的可选项)。新工程将自动包含测试的target,并且相关框架也搭建完毕,可以说测试终于摆脱了iOS开发中“二等公民”的地位,现在已经变 得和产品代码一样重要了。我相信每个工程师在完成自己的业务代码的同时,也有最基本的编写和维护相应的测试代码的义务,以保证自己的代码能够正确运行。更 进一步,如果能够使用TDD来进行开发,不仅能保证代码运行的正确性,也有助于代码结构的安排和思考,有助于自身的不断提高。我在最开始进行开发时也曾对 测试嗤之以鼻,但后来无数的惨痛教训让我明白那么多工程师痴迷于测试或者追求更完美的测试,是有其深刻含义的。如果您之前还没有开始为您的代码编写测试, 我强烈建议,从今天开始,从现在开始(也许做不到的话,也请从下一个项目开始),编写测试,或者尝试一下TDD的开发方式。 而Kiwi是一个iOS平台十分好用的行为驱 动开发(Behavior Driven Development,以下简称BDD)的测试框架,有着非常漂亮的语法,可以写出结构性强,非常容易读懂的测试。因为国内现在有关Kiwi的介绍比较 少,加上在测试这块很能很多工程师们并没有特别留意,水平层次可能相差会很远,因此在这一系列的两篇博文中,我将从头开始先简单地介绍一些TDD的概念和 思想,然后从XCTest的最简单的例子开始,过渡到Kiwi的测试世界。在下一篇中我将继续深入介绍一些Kiwi的其他稍高一些的特性,以期更多的开发 者能够接触并使用Kiwi这个优秀的测试框架。 什么是TDD,为什么我们要TDD测试驱动开发并不是一个很新鲜的概念了。软件开发工程师们(当然包括你我)最开始学习程序编写时,最喜欢干的事情就是编写一段代码,然后运行观察结 果是否正确。如果不对就返回代码检查错误,或者是加入断点或者输出跟踪程序并找出错误,然后再次运行查看输出是否与预想一致。如果输出只是控制台的一个简 单的数字或者字符那还好,但是如果输出必须在点击一系列按钮之后才能在屏幕上显示出来的东西呢?难道我们就只能一次一次地等待编译部署,启动程序然后操作 UI,一直点到我们需要观察的地方么?这种行为无疑是对美好生命和绚丽青春的巨大浪费。于是有一些已经浪费了无数时间的资深工程师们突然发现,原来我们可 以在代码中构建出一个类似的场景,然后在代码中调用我们之前想检查的代码,并将运行的结果与我们的设想结果在程序中进行比较,如果一致,则说明了我们的代 码没有问题,是按照预期工作的。比如我们想要实现一个加法函数add,输入两个数字,输出它们相加后的结果。那么我们不妨设想我们真的拥有两个数,比如3 和5,根据人人会的十以内的加法知识,我们知道答案是8.于是我们在相加后与预测的8进行比较,如果相等,则说明我们的函数实现至少对于这个例子是没有问 题的,因此我们对“这个方法能正确工作”这一命题的信心就增加了。这个例子的伪码如下: //Product Codeadd(float num1,float num 2) {...}//Test codelet a = 3; let b = 5; let c = a + b;if (c == 8) { // Yeah,it works!} else { //Something wrong!} 当测试足够全面和具有代表性的时候,我们便可以信心爆棚,拍着胸脯说,这段代码没问题。我们做出某些条件和假设,并以其为条件使用到被测试代码中,并比较预期的结果和实际运行的结果是否相等,这就是软件开发中测试的基本方式。 而TDD是一种相对于普通思维的方式来说,比较极端的一种做法。我们一般能想到的是先编写业务代码,也就是上面例子中的 在TDD原则的指导下,我们先编写测试代码。这时因为还没有对应的产品代码,所以测试代码肯定是无法通过的。在大多数测试系统中,我们使用红色来表 示错误,因此一个测试的初始状态应该是红色的。接下来我们需要使用最小的代价(最少的代码)来让测试通过。通过的测试将被表示为安全的绿色,于是我们回到 了绿色的状态。接下来我们可以添加一些测试例,来验证我们的产品代码的实现是否正确。如果不幸新的测试例让我们回到了红色状态,那我们就可以修改产品代 码,使其回到绿色。如此反复直到各种边界和测试都进行完毕,此时我们便可以得到一个具有测试保证,鲁棒性超强的产品代码。在我们之后的开发中,因为你有这 些测试的保证,你可以大胆重构这段代码或者与之相关的代码,最后只需要保证项目处于绿灯状态,你就可以保证代码没重构没有出现问题。 简单说来,TDD的基本步骤就是“红→绿→大胆重构”。 使用XCTest来执行TDDXcode 5中已经集成了XCTest的测试框架(之前版本是SenTestingKit和OCUnit),所谓测试框架,就是一组让“将测试集成到工程中”以及 “编写和实践测试”变得简单的库。我们之后将通过实现一个栈数据结构的例子,来用XCTest初步实践一下TDD开发。在大家对TDD有一些直观认识之 后,再转到Kiwi的介绍。如果您已经在使用XCTest或者其他的测试框架了的话,可以直接跳过本节。 首先我们用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 可以看到, 接下来让我们想想要做什么吧。我们要实现一个简单的栈数据结构,那么当然会有一个类来代表这种数据结构,在这个工程中我打算就叫它 - (void)testStackExist { XCTAssertNotNil([VVStack class],@"VVStack class should exist."); } - (void)testStackObjectCanBeCreated { VVStack *stack = [VVStack new]; XCTAssertNotNil(stack,@"VVStack object can be created."); } 嘛,当然是不可能通过测试的,而且甚至连编译都无法完成,因为我们现在根本没有一个叫做 由于 - (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."); } 因为我们还没有实现 //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 还不熟悉的时候,我们有必要放慢节奏和动作,将整个开发理念进行充分实践,这样才有可能在之后更复杂的案例中正确使用。于是我们发扬不怕繁杂,精益求精的 精神,在刚才的测试例上增加一个测试,回到 - (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"); } 很好,这下子我们回到了红灯状态,这正是我们所期望的,现在是时候来考虑实现这个栈了。这个实现过于简单,也有非常多的思路,其中一种是使用一个 //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 测试通过,注意到在 接下来我们可以模仿继续实现 Kiwi和BDD的测试思想
行为驱动开发(BDD)正是为了解决上述问题而生的,作为第二代敏捷方法,BDD提倡的是通过将测试语句转换为类似自然语言的描述,开发人员可以使 用更符合大众语言的习惯来书写测试,这样不论在项目交接/交付,或者之后自己修改时,都可以顺利很多。如果说作为开发者的我们日常工作是写代码,那么 BDD其实就是在讲故事。一个典型的BDD的测试用例包活完整的三段式上下文,测试大多可以翻译为 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]; }); }); }); 我们很容易根据上下文将其提取为
很简单啊有木有!在这样的语法下,是不是写测试的兴趣都被激发出来了呢。关于Kiwi的进一步语法和使用,我们稍后详细展开。首先来看看如何在项目中添加Kiwi框架吧。 在项目中添加Kiwi最简单和最推荐的方法当然是CocoaPods,如果您对CocoaPods还比较陌生的话,推荐您花时间先看一看这篇CocoaPods的简介。Xcode 5和XCTest环境下,我们需要在Podfile中添加类似下面的条目(记得将 target :VVStackTests,:exclusive => true do pod 'Kiwi/XCTest'end 之后 行为描述(Specs)和期望(Expectations),Kiwi测试的基本结构我们先来新建一个Kiwi测试吧。如果安装了Kiwi的Template的话,在新建文件中选择 #import <Kiwi/Kiwi.h>SPEC_BEGIN(SimpleStringSpec) describe(@"SimpleString",^{ }); SPEC_END 你可能会觉得这不是objc代码,甚至怀疑这些语法是否能够编译通过。其实 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"]; }); }); });
VVStack[36517:70b] + 'SimpleString,when assigned to 'Hello world',should exist' [PASSED] VVStack[36517:70b] + 'SimpleString,should equal to 'Hello world'' [PASSED] 可以看到,这三个关键字的描述将在测试时被依次打印出来,形成一个完整的行为描述。除了这三个之外,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]; }); }); }); 看到这里的您看这段测试应该不成问题。需要注意的有两点:首先 接下来我们再为这个context添加一个测试例,用来测试初始状况时栈是否为空。因为我们使用了一个Array来作为存储容器,根据我们之前用过的equal方法,我们很容易想到下面这样的测试代码 it(@"should equal contains 0 element",^{ [[theValue([stack.numbers count]) should] equal:theValue(0)]; }); 这段测试在逻辑上没有太大问题,但是有非常多值得改进的地方。首先如果我们需要将原来写在Extension里的 //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有一些特殊处理,比如 it(@"should equal contains 0 element",^{ [[stack should] haveCountOf:0]; }); 在这种情况下,我们并没有显式地调用VVStack的 其实对于这个测试,我们还可以写出更漂亮的版本,像这样: it(@"should equal contains 0 element",^{ [[stack should] beEmpty]; }); 好了。关于空栈这个情景下的测试感觉差不多了。我们继续用TDD的思想来完善 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这样的基础类时,就不需要等到或者在真实的环境中检测了。因为在被别人使用之前,我们自己的测试代码已经能够保证它的正确性了。 it(@"should raise a exception when pop",^{ [[theBlock(^{ [stack pop]; }) should] raiseWithName:@"VVStackPopEmptyException"]; }); 和 - (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; } 进一步的KiwiVVStack的测试和实现就到这里吧,根据这套测试,您可以使用自己的实现来轻易地重构这个类,而不必担心破坏它的公共接口的行为。如果需要添加 新的功能或者修正已有bug的时候,我们也可以通过添加或者修改相应的测试,来确保正确性。我将会在下一篇博文中继续介绍Kiwi,看看Kiwi在异步测 试和mock/stub的使用和表现如何。Kiwi现在还在比较快速的发展中,官方repo的wiki上有一些不错的资料和文档,可以参考。 来自:OneV's Den 如果你喜欢这篇文章,欢迎分享给更多的朋友(点击【右上角图标 -> 分享到朋友圈】)。
iOS开发 iOSDevTip
(编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |