可以看到,VVStackTests是XCTestCase的子类,而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 |
测试通过,注意到在-testStackObjectCanBeCreated和testPushANumberAndGetIt两个测试中都生成了一个VVStack对象。在这个测试文件中基本每个测试都会需要初始化对象,因此我们可以考虑在测试文件中添加一个VVStack的实例成员,并将测试中的初始化代码移到-setUp中,并在-tearDown中释放。
接下来我们可以模仿继续实现pop等栈的方法。鉴于篇幅这里不再继续详细实现,大家可以自己动手试试看。记住先实现测试,然后再实现产品代码。一开始您可能会觉得这很无聊,效率低下,但是请记住这是起步练习不可缺少的一部分,而且在我们的例子中其实一切都是以“慢动作”在进行的。相信在经过实践和使用后,您将会逐渐掌握自己的节奏和重点测试。关于使用XCTest到这里为止的代码,可以在github上找到。