AAA 原則
那,我們來試試看單元測試這條途徑。
在 Xcode 裡頭建立專案的時候,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");
}