AAA 原则
那,我们来试试看单元测试这条途径。
在Xcode里头建立Project的时候,Xcode会帮我们的App同时建立一个单元测试的Bundle—其实苹果也鼓励你写单元测试—在这个Bundle中,会出现一个继承自XCTestCase的class,在里头撰写任何用test开头的method,像-testHit
,都是一条test case。也就是,我们写测试的时候,就是写出一群用test为开头的method。
Xcode从5.1版现在的测试框架叫做XCTest,在这之前则是使用一套叫做OCUnit的测试框架(这边的OC其实就是Objective-C的意思),除此之外, iOS与Mac OS X平台上还有许多有名的测试框架,像是GHUnit、Kiwi等等。我们在这边只先讲解XCUnit,如果你对单元测试有些心得,觉得XCUnit已经无法满足你的需求,不妨也可以试试看其他的测试框架。
在撰写测试的时候,基本原则是一次只测试一项function 或method,同时一个test case 会包含所谓的AAA 原则:Arrange、Act 与Assert。
- Arrange: 先设定我们在这次测试中,所预期的结果
- Act: 就是我们想要测试的function 或method
- Assert: 确认在Action 发生后,确认在执行了想要测试function 或method 后,的确符合我们在Arrange 阶段设定的目标
举个例子,我们预期一条长度为6 、正在往左边移动的蛇,在先往上走一格、再往右走一格、再往下走一格之后,这条蛇的头一定会撞到自己的身体,如果我们的程式说蛇头没有撞到,就一定有Bug。就可以拆解成:
- Arrange: 头应该会撞到身体
- Action: 让蛇执行往上右下移动的动作
- Assert: 确认头真的撞到身体了
这个case 或许会像这样:
- (void)testHit
{
KKSnake *snake = [[KKSnake alloc]
initWithWorldSize:KKMakeSnakeWorldSize(10, 10) length:6];
[snake changeDirection:KKSnakeDirectionUp];[[snake move];
[snake changeDirection:KKSnakeDirectionRight];[snake move];
[snake changeDirection:KKSnakeDirectionDown];[snake move];
XCTAssertEqual([snake isHeadHitBody], YES, @"must hit the body.");
}
如果我们想要测试「蛇的尾巴加长」这段程式是否正常,大概会这么写:原本这条蛇的长度为2,尾巴位在(6, 5),假如蛇的身体要加长两格,我们预期蛇的长度变成4,尾巴位在(8, 5)。
- (void)testIncreaseLength
{
ZBSnake *snake = [[ZBSnake alloc] initWithWorldSize:ZBMakeSnakeWorldSize(10, 10) length:2];
XCTAssertEqual((int)[snake.points count], 2, @"Length must be 2 but %d", [snake.points count]);
NSInteger x;
NSInteger y;
x = [snake.points[[snake.points count] - 1] snakePointValue].x;
y = [snake.points[[snake.points count] - 1] snakePointValue].y;
XCTAssertEqual(x, 6, @"must be 6");
XCTAssertEqual(y, 5, @"must be 5");
[snake increaseLength:2];
XCTAssertEqual((int)[snake.points count], 4, @"Length must be 4 but %d", [snake.points count]);
x = [snake.points[[snake.points count] - 1] snakePointValue].x;
y = [snake.points[[snake.points count] - 1] snakePointValue].y;
XCTAssertEqual(x, 8, @"must be 8");
XCTAssertEqual(y, 5, @"must be 5");
}