簡介:
單元測試是軟件開發(fā)的一個重要方面。畢竟,單元測試可以幫你找到bug和崩潰原因,而程序崩潰是Apple在審查時拒絕app上架的首要原因。
單元測試不是萬能的,但Apple把它作為開發(fā)工具包的一部分,不僅讓你創(chuàng)作的APP更穩(wěn)定,而且提供了一致、有趣的用戶體驗,這些都是讓用戶給你五星評價的源泉!iOS7提供了一個升級的單元測試框架,讓你在Xcode中運(yùn)行單元測試更為容易。當(dāng)你完成這一章節(jié),你將學(xué)會如何給現(xiàn)有app添加測試——并有可能培養(yǎng)出對編寫測試的熱愛!
/*
本文翻譯自《iOS7 by Tutorials》一書的第十一章“Unit Testing in Xcode 5”,想體會原文精髓的朋友請到Raywenderlich商店支持正版。
——————(博客園、新浪微博)葛布林大帝
*/
目錄:
一、單元測試基礎(chǔ)
二、開始項目
三、下一步何去何從?
四、挑戰(zhàn)
附錄:XCTest斷言參考
一、單元測試基礎(chǔ)
在過去,Xcode引入了一個叫做OCUnit的開源單元測試框架。而在Xcode 5里面,Apple發(fā)布了他們自己的的單元測試框架,叫做XCTest。
如果你已經(jīng)熟悉OCUnit,別擔(dān)心,XCTest是一個建立在OCUnit之上并且十份相似的API。從OCUnit過渡到XCTest非常簡單,要做的僅僅是把STFail替換為XCTFail、STAssert替換為XCTAssert等等諸如此類。如果你已經(jīng)熟悉這些基礎(chǔ),可以直接跳到下一節(jié)。
1.高層次概述
單元測試有四個層級。從上到下,它們分別是:
理論很美好,但有時舉例會更容易闡述事物。用Empty application 模板創(chuàng)建一個新項目,命名為EmptyApp。 Xcode模板會自動包含一個叫做EmptyAPPTests的test target,添加到EmptyApp的 app target里,如下圖:
注意測試用例類包含了一個沒有關(guān)聯(lián)頭文件的.m文件,打開EmptyAppTests.m看看第一個測試用例的源代碼。
測試方法必須以單詞test開始,以便test runner能找到它們。在你的示例項目里,測試類包含了一個測試方法,叫做testExample。
setUp和tearDown方法就像守護(hù)在測試用例周圍的衛(wèi)兵一樣。
把所有對象的程序設(shè)置代碼或重復(fù)性代碼放到setUp里,使測試用例方法保持清爽、高效。
類似的,關(guān)閉文件句柄或取消掛起網(wǎng)絡(luò)請求等清理活動的方法應(yīng)該放到tearDown里。
Test runner 會依次調(diào)用setUp、testExample和tearDown方法。如果你申明了第二個測試方法testSecondExample,Test runner會依次調(diào)用setUp、testSecondExample,最后是tearDown方法。如果你有多個測試方法,setUp和tearDown會在一個測試環(huán)節(jié)調(diào)用多次——每經(jīng)過一個測試用例方法調(diào)用一次!
這個故事的寓意是不要放任何處理太慢或處理頻繁的東西到setUp或tearDown方法里——這會讓你運(yùn)行測試套件時面臨漫長的等待!
2.創(chuàng)建你的第一個測試
testExample方法只有一個叫做XCTFail的語句,正如它名字里暗示的:總是會失敗。這個語句不是非常有用,你可以寫一個比它更好的!刪除testExample方法,并添加如下方法:
- (void)test_addition_twoPlusTwo_isFour
{ XCTAssert(2 + 2 == 4, @"2 + 2 should be 4 but %d was returned instead", 2+2);}
測試用例的一個常用命名標(biāo)準(zhǔn)是:unitOfWork_stateUnderTest_expectedBehavior (工作單元_測試狀態(tài)_預(yù)期行為)。
在這個例子里,被測試的工作單元是加法,測試狀態(tài)是2 + 2,預(yù)期行為是結(jié)果為4。
所有XCTest斷言都有前綴XCT。XCTAssert是可用于單元測試的簡單斷言,第一個參數(shù)是預(yù)評估為ture的表達(dá)式,當(dāng)斷言失敗時,其后NSLog風(fēng)格的參數(shù)會顯示一條消息。
確保項目的當(dāng)前target為iphone模擬器,通過窗口頂部目錄的PRoduct -> Test(Command-U)來運(yùn)行測試,模擬器會啟動并執(zhí)行測試套件。如果通知處于激活狀態(tài),你會看到下列確認(rèn)消息:
為了證實第一個單元測試成功,切換到Test Navigator,箭頭指出了它:
哈哈!翠綠色的小勾旁邊顯示出了你的單元測試。
你還可以看到邊框空白處菱形圖標(biāo)旁的代碼,如下所示:
這些圖標(biāo)展示關(guān)聯(lián)測試代碼的狀態(tài):
@implementation旁的綠色小勾表示這個類測試通過,test_addition_twoPlusTwo_isFour旁的綠色小勾表示這個方法測試通過。
同時,這些圖標(biāo)也是按鈕:
點擊@implementation旁的圖標(biāo)將會運(yùn)行這個類的所有測試,點擊其他測試方法旁的圖標(biāo)則會運(yùn)行該測試方法,試一試吧!
現(xiàn)在你已經(jīng)對測試的概念和執(zhí)行有了初步了解,是時候開始本章的示例項目了——測試開始!
二、開始項目
本章的剩余部分你將使用一個名為Reversi的黑白棋游戲項目,規(guī)則:兩個玩家,分別代表白方和黑方,輪流在8x8棋盤上落子。通過包圍對方棋子來吃掉它,游戲結(jié)束時棋子最多的為勝者。
如何創(chuàng)建這個游戲,請看:http://www.raywenderlich.com/29228/how-to-develop-an-ipad-board-game-app-part-12
下載本文頁尾提供的示例項目并運(yùn)行,點擊屏幕下方的Vs Computer按鈕與電腦進(jìn)行對戰(zhàn),感受一下這個游戲的界面和玩法。
你獲勝了嗎?或者被AI對手爆出翔?不管怎樣,你的工作不是整日玩游戲——是時候添加一些有用的測試到項目里了。
1.添加測試的支持
第一個需要單元測試的是GameBoard類。這個類囊括了8x8棋盤的基本邏輯,64個單元格中的每個都有一個狀態(tài)——空、黑棋或白棋——并且GameBoard實例讓你能獲取并設(shè)置每一個方塊的狀態(tài)。
打開GameBoard.h看一下里面的方法,在開始為現(xiàn)有代碼編寫測試之前,弄清楚各方法的作用和實現(xiàn)是一個好主意。
在GameBoard.h,你會看到下列兩個方法:
// gets the state of the cell at the given location// raises an NSRangeException if the column or row are out of bounds- (BoardCellState) cellStateAtColumn:(NSInteger)column andRow:(NSInteger)row;// sets the state of the cell at the given location// raises an NSRangeException if the column or row are out of bounds- (void) setCellState:(BoardCellState)state forColumn:(NSInteger)column andRow:(NSInteger)row;
cellStateAtColumn:andRow: 和 setCellState:forColumn:andRow: 由你非常熟悉的getter/setter模式里發(fā)展出來,你的第一個測試是執(zhí)行如下動作:
第一步是創(chuàng)建一個GameBoard測試類,右擊ReversiGameTests分組,選擇 iOS/Cocoa Touch/Objective-C test case class 創(chuàng)建一個名為GameBoardTests的測試類,繼承自XCTestCase。
確保你的新測試用例添加到ReversiGameTests target,如下圖(這個步驟非常重要,如果沒添加到正確的target里,你的測試不會運(yùn)行):
打開 GameBoardTests.m 并且刪除 testExample 方法,你不需要它。
然后在 GameBoardTests.m 頂部導(dǎo)入頭文件(這僅僅是讓你的測試類能夠訪問GameBoard類):GameBoard.h
#import "GameBoard.h"
你需要為你的所有測試提供一個GameBoard 實例,創(chuàng)建一個實例變量會比在每個測試?yán)锷昝饕粋€清爽得多。
在GameBoardTests.m 里更新@interface 如下:
@interface GameBoardTests : XCTestCase
{
GameBoard *_board;}
現(xiàn)在你有了_board實例變量,可以開始測試了。
setUp 方法是第一次初始化_board的好地方,修改setUp如下:
- (void)setUp{ [super setUp]; _board = [[GameBoard alloc] init]; }
現(xiàn)在這個類的所有測試用例方法都能夠訪問初始化后的_board實例變量了。
2.第一個測試
這是你需要為首個測試用例添加的所有步驟,添加以下方法到GameBoardTests.m:
- (void)test_setAndGetCellState_setValidCell_cellStateChanged { [_board setCellState:BoardCellStateWhitePiece forColumn:4 andRow:5];
BoardCellState retrievedState = [_board cellStateAtColumn:4 andRow:5];
XCTAssertEqual(BoardCellStateWhitePiece, retrievedState, @"The cell should be white!");}
上面的代碼在(4,5)單元格里設(shè)置了一個白棋,并且立刻檢索了相同單元格的狀態(tài)。XCTAssertEqual 斷言檢查它們是否相等,如果不相等,你會看到一個異常信息,然后你將得知有一些東西需要檢查。
上面代碼的方法名遵循我之前提到的格式,通過這個方法名,你可以很容易看出它通過設(shè)置正確的單元格位置來測試setter和getter方法,并期待單元格狀態(tài)的改變。
如果你的測試工作是有計劃的,確保iPhone和iPad模擬器都測試,然后運(yùn)行測試(Command-U)。
切換到Test Navigator,你會看到一個綠色小勾表示測試通過,如下圖:
這看起來只是一個簡單的測試,但是它在調(diào)試錯誤里提供了巨大的價值。
在內(nèi)部, GAMEBOARD類使用一個簡單的二維數(shù)組來跟蹤8X8棋盤。但如果你曾經(jīng)改變了代表向量或矩陣的數(shù)組,本次測試將作為回歸測試,確保interface 的基礎(chǔ)仍在工作。
作為一個附帶的好處,為現(xiàn)有的類編寫測試可以大大有助于理解代碼是如何工作的。分析類的方法可以幫助你辨別其功能,并為你編寫測試提供便利。
3.測試異常
按照設(shè)計的功能測試代碼有助于確保其正確性,但也使得你的app“早早失敗或高調(diào)失敗”——那些異常游戲狀態(tài)或無效條件被調(diào)試器很快抓住。
GameBoard.h里cellStateAtColumn:andRow: 和 setCellState:forColumn:andRow: 方法的注釋表明,如果行或列超出棋盤邊框,它們會彈出錯誤。看起來你已經(jīng)找到更多的測試條件。
添加下列兩個方法:
- (void)test_setCellState_withInvalidCoords_exceptionThrown {XCTAssertThrowsSpecificNamed([_board setCellState:BoardCellStateBlackPieceforColumn:10andRow:7], NSException,NSRangeException,@"Out-of-bounds board set should raise an exception");}- (void)test_getCellState_withInvalidCoords_exceptionThrown {XCTAssertThrowsSpecificNamed([_board cellStateAtColumn:7 andRow:-10],NSException,NSRangeException,@"Out-of-bounds board access should raise an exception");}
上面的代碼里,test_setCellState_withInvalidCoords_exceptionThrown: 試圖設(shè)置超出范圍的單元格(10,7),同時test_setCellState_withInvalidCoords_exceptionThrown: 試圖獲取超出范圍的單元格(7,-10)。再次的,方法名已指出在正測試不正確的坐標(biāo),報出異常正在意料之中。
XCTAssertThrowsSpecificNamed 采用以下四點作為參數(shù):
點擊Command-U運(yùn)行測試,你應(yīng)該看到以下結(jié)果
這是什么?你希望用出色的代碼通過測試,但是兩個錯誤標(biāo)記在Issue Navigator上。測試失敗信息也會顯示在代碼上,如下圖:
所有的測試失敗消息為:
[GameBoardTests test_getCellState_withInvalidCoords_exceptionThrown] failed: (([_board cellStateAtColumn:7 andRow:-10]) throws <NSException, "NSRangeException">) failed: throwing <NSException, "NSGenericException", "row or column out of bounds"> - Out-of- bounds board access should raise an exception
如果你分解上面的消息,你會看到你希望的行為是(throws <NSException, "NSRangeException">) ,而實際發(fā)生的是(throwing <NSException, "NSGenericException">) 。
在這個例子里,你期待的是NSRangeException ,但接收到的卻是NSGenericException 。
看起來你已經(jīng)做了一些研究!
示例項目地址:http://pan.baidu.com/s/1o6x6zxg
新聞熱點
疑難解答