簡(jiǎn)介:
單元測(cè)試是軟件開發(fā)的一個(gè)重要方面。畢竟,單元測(cè)試可以幫你找到bug和崩潰原因,而程序崩潰是Apple在審查時(shí)拒絕app上架的首要原因。
單元測(cè)試不是萬能的,但Apple把它作為開發(fā)工具包的一部分,不僅讓你創(chuàng)作的APP更穩(wěn)定,而且提供了一致、有趣的用戶體驗(yàn),這些都是讓用戶給你五星評(píng)價(jià)的源泉!iOS7提供了一個(gè)升級(jí)的單元測(cè)試框架,讓你在Xcode中運(yùn)行單元測(cè)試更為容易。當(dāng)你完成這一章節(jié),你將學(xué)會(huì)如何給現(xiàn)有app添加測(cè)試——并有可能培養(yǎng)出對(duì)編寫測(cè)試的熱愛!
/*
本文翻譯自《iOS7 by Tutorials》一書的第十一章“Unit Testing in Xcode 5”,想體會(huì)原文精髓的朋友請(qǐng)到Raywenderlich商店支持正版。
——————(博客園、新浪微博)葛布林大帝
*/
目錄:
一、單元測(cè)試基礎(chǔ)
二、開始項(xiàng)目
三、下一步何去何從?
四、挑戰(zhàn)
附錄:XCTest斷言參考
一、單元測(cè)試基礎(chǔ)
在過去,Xcode引入了一個(gè)叫做OCUnit的開源單元測(cè)試框架。而在Xcode 5里面,Apple發(fā)布了他們自己的的單元測(cè)試框架,叫做XCTest。
如果你已經(jīng)熟悉OCUnit,別擔(dān)心,XCTest是一個(gè)建立在OCUnit之上并且十份相似的API。從OCUnit過渡到XCTest非常簡(jiǎn)單,要做的僅僅是把STFail替換為XCTFail、STAssert替換為XCTAssert等等諸如此類。如果你已經(jīng)熟悉這些基礎(chǔ),可以直接跳到下一節(jié)。
1.高層次概述
單元測(cè)試有四個(gè)層級(jí)。從上到下,它們分別是:
理論很美好,但有時(shí)舉例會(huì)更容易闡述事物。用Empty application 模板創(chuàng)建一個(gè)新項(xiàng)目,命名為EmptyApp。 Xcode模板會(huì)自動(dòng)包含一個(gè)叫做EmptyAPPTests的test target,添加到EmptyApp的 app target里,如下圖:

注意測(cè)試用例類包含了一個(gè)沒有關(guān)聯(lián)頭文件的.m文件,打開EmptyAppTests.m看看第一個(gè)測(cè)試用例的源代碼。
測(cè)試方法必須以單詞test開始,以便test runner能找到它們。在你的示例項(xiàng)目里,測(cè)試類包含了一個(gè)測(cè)試方法,叫做testExample。
setUp和tearDown方法就像守護(hù)在測(cè)試用例周圍的衛(wèi)兵一樣。
把所有對(duì)象的程序設(shè)置代碼或重復(fù)性代碼放到setUp里,使測(cè)試用例方法保持清爽、高效。
類似的,關(guān)閉文件句柄或取消掛起網(wǎng)絡(luò)請(qǐng)求等清理活動(dòng)的方法應(yīng)該放到tearDown里。
Test runner 會(huì)依次調(diào)用setUp、testExample和tearDown方法。如果你申明了第二個(gè)測(cè)試方法testSecondExample,Test runner會(huì)依次調(diào)用setUp、testSecondExample,最后是tearDown方法。如果你有多個(gè)測(cè)試方法,setUp和tearDown會(huì)在一個(gè)測(cè)試環(huán)節(jié)調(diào)用多次——每經(jīng)過一個(gè)測(cè)試用例方法調(diào)用一次!
這個(gè)故事的寓意是不要放任何處理太慢或處理頻繁的東西到setUp或tearDown方法里——這會(huì)讓你運(yùn)行測(cè)試套件時(shí)面臨漫長的等待!
2.創(chuàng)建你的第一個(gè)測(cè)試
testExample方法只有一個(gè)叫做XCTFail的語句,正如它名字里暗示的:總是會(huì)失敗。這個(gè)語句不是非常有用,你可以寫一個(gè)比它更好的!刪除testExample方法,并添加如下方法:
- (void)test_addition_twoPlusTwo_isFour
{ XCTAssert(2 + 2 == 4, @"2 + 2 should be 4 but %d was returned instead", 2+2);}測(cè)試用例的一個(gè)常用命名標(biāo)準(zhǔn)是:unitOfWork_stateUnderTest_expectedBehavior (工作單元_測(cè)試狀態(tài)_預(yù)期行為)。
在這個(gè)例子里,被測(cè)試的工作單元是加法,測(cè)試狀態(tài)是2 + 2,預(yù)期行為是結(jié)果為4。
所有XCTest斷言都有前綴XCT。XCTAssert是可用于單元測(cè)試的簡(jiǎn)單斷言,第一個(gè)參數(shù)是預(yù)評(píng)估為ture的表達(dá)式,當(dāng)斷言失敗時(shí),其后NSLog風(fēng)格的參數(shù)會(huì)顯示一條消息。
確保項(xiàng)目的當(dāng)前target為iphone模擬器,通過窗口頂部目錄的PRoduct -> Test(Command-U)來運(yùn)行測(cè)試,模擬器會(huì)啟動(dòng)并執(zhí)行測(cè)試套件。如果通知處于激活狀態(tài),你會(huì)看到下列確認(rèn)消息:

為了證實(shí)第一個(gè)單元測(cè)試成功,切換到Test Navigator,箭頭指出了它:

哈哈!翠綠色的小勾旁邊顯示出了你的單元測(cè)試。
你還可以看到邊框空白處菱形圖標(biāo)旁的代碼,如下所示:

這些圖標(biāo)展示關(guān)聯(lián)測(cè)試代碼的狀態(tài):
@implementation旁的綠色小勾表示這個(gè)類測(cè)試通過,test_addition_twoPlusTwo_isFour旁的綠色小勾表示這個(gè)方法測(cè)試通過。
同時(shí),這些圖標(biāo)也是按鈕:
點(diǎn)擊@implementation旁的圖標(biāo)將會(huì)運(yùn)行這個(gè)類的所有測(cè)試,點(diǎn)擊其他測(cè)試方法旁的圖標(biāo)則會(huì)運(yùn)行該測(cè)試方法,試一試吧!
現(xiàn)在你已經(jīng)對(duì)測(cè)試的概念和執(zhí)行有了初步了解,是時(shí)候開始本章的示例項(xiàng)目了——測(cè)試開始!
二、開始項(xiàng)目
本章的剩余部分你將使用一個(gè)名為Reversi的黑白棋游戲項(xiàng)目,規(guī)則:兩個(gè)玩家,分別代表白方和黑方,輪流在8x8棋盤上落子。通過包圍對(duì)方棋子來吃掉它,游戲結(jié)束時(shí)棋子最多的為勝者。

如何創(chuàng)建這個(gè)游戲,請(qǐng)看:http://www.raywenderlich.com/29228/how-to-develop-an-ipad-board-game-app-part-12
下載本文頁尾提供的示例項(xiàng)目并運(yùn)行,點(diǎn)擊屏幕下方的Vs Computer按鈕與電腦進(jìn)行對(duì)戰(zhàn),感受一下這個(gè)游戲的界面和玩法。
你獲勝了嗎?或者被AI對(duì)手爆出翔?不管怎樣,你的工作不是整日玩游戲——是時(shí)候添加一些有用的測(cè)試到項(xiàng)目里了。
1.添加測(cè)試的支持
第一個(gè)需要單元測(cè)試的是GameBoard類。這個(gè)類囊括了8x8棋盤的基本邏輯,64個(gè)單元格中的每個(gè)都有一個(gè)狀態(tài)——空、黑棋或白棋——并且GameBoard實(shí)例讓你能獲取并設(shè)置每一個(gè)方塊的狀態(tài)。
打開GameBoard.h看一下里面的方法,在開始為現(xiàn)有代碼編寫測(cè)試之前,弄清楚各方法的作用和實(shí)現(xiàn)是一個(gè)好主意。
在GameBoard.h,你會(huì)看到下列兩個(gè)方法:
// 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ā)展出來,你的第一個(gè)測(cè)試是執(zhí)行如下動(dòng)作:
第一步是創(chuàng)建一個(gè)GameBoard測(cè)試類,右擊ReversiGameTests分組,選擇 iOS/Cocoa Touch/Objective-C test case class 創(chuàng)建一個(gè)名為GameBoardTests的測(cè)試類,繼承自XCTestCase。
確保你的新測(cè)試用例添加到ReversiGameTests target,如下圖(這個(gè)步驟非常重要,如果沒添加到正確的target里,你的測(cè)試不會(huì)運(yùn)行):

打開 GameBoardTests.m 并且刪除 testExample 方法,你不需要它。
然后在 GameBoardTests.m 頂部導(dǎo)入頭文件(這僅僅是讓你的測(cè)試類能夠訪問GameBoard類):GameBoard.h
#import "GameBoard.h"
你需要為你的所有測(cè)試提供一個(gè)GameBoard 實(shí)例,創(chuàng)建一個(gè)實(shí)例變量會(huì)比在每個(gè)測(cè)試?yán)锷昝饕粋€(gè)清爽得多。
在GameBoardTests.m 里更新@interface 如下:
@interface GameBoardTests : XCTestCase
{
GameBoard *_board;}
現(xiàn)在你有了_board實(shí)例變量,可以開始測(cè)試了。
setUp 方法是第一次初始化_board的好地方,修改setUp如下:
- (void)setUp{ [super setUp]; _board = [[GameBoard alloc] init]; }現(xiàn)在這個(gè)類的所有測(cè)試用例方法都能夠訪問初始化后的_board實(shí)例變量了。
2.第一個(gè)測(cè)試
這是你需要為首個(gè)測(cè)試用例添加的所有步驟,添加以下方法到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è)置了一個(gè)白棋,并且立刻檢索了相同單元格的狀態(tài)。XCTAssertEqual 斷言檢查它們是否相等,如果不相等,你會(huì)看到一個(gè)異常信息,然后你將得知有一些東西需要檢查。
上面代碼的方法名遵循我之前提到的格式,通過這個(gè)方法名,你可以很容易看出它通過設(shè)置正確的單元格位置來測(cè)試setter和getter方法,并期待單元格狀態(tài)的改變。
如果你的測(cè)試工作是有計(jì)劃的,確保iPhone和iPad模擬器都測(cè)試,然后運(yùn)行測(cè)試(Command-U)。
切換到Test Navigator,你會(huì)看到一個(gè)綠色小勾表示測(cè)試通過,如下圖:

這看起來只是一個(gè)簡(jiǎn)單的測(cè)試,但是它在調(diào)試錯(cuò)誤里提供了巨大的價(jià)值。
在內(nèi)部, GAMEBOARD類使用一個(gè)簡(jiǎn)單的二維數(shù)組來跟蹤8X8棋盤。但如果你曾經(jīng)改變了代表向量或矩陣的數(shù)組,本次測(cè)試將作為回歸測(cè)試,確保interface 的基礎(chǔ)仍在工作。
作為一個(gè)附帶的好處,為現(xiàn)有的類編寫測(cè)試可以大大有助于理解代碼是如何工作的。分析類的方法可以幫助你辨別其功能,并為你編寫測(cè)試提供便利。
3.測(cè)試異常
按照設(shè)計(jì)的功能測(cè)試代碼有助于確保其正確性,但也使得你的app“早早失敗或高調(diào)失敗”——那些異常游戲狀態(tài)或無效條件被調(diào)試器很快抓住。
GameBoard.h里cellStateAtColumn:andRow: 和 setCellState:forColumn:andRow: 方法的注釋表明,如果行或列超出棋盤邊框,它們會(huì)彈出錯(cuò)誤。看起來你已經(jīng)找到更多的測(cè)試條件。
添加下列兩個(gè)方法:
- (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),同時(shí)test_setCellState_withInvalidCoords_exceptionThrown: 試圖獲取超出范圍的單元格(7,-10)。再次的,方法名已指出在正測(cè)試不正確的坐標(biāo),報(bào)出異常正在意料之中。
XCTAssertThrowsSpecificNamed 采用以下四點(diǎn)作為參數(shù):
點(diǎn)擊Command-U運(yùn)行測(cè)試,你應(yīng)該看到以下結(jié)果

這是什么?你希望用出色的代碼通過測(cè)試,但是兩個(gè)錯(cuò)誤標(biāo)記在Issue Navigator上。測(cè)試失敗信息也會(huì)顯示在代碼上,如下圖:

所有的測(cè)試失敗消息為:
[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如果你分解上面的消息,你會(huì)看到你希望的行為是(throws <NSException, "NSRangeException">) ,而實(shí)際發(fā)生的是(throwing <NSException, "NSGenericException">) 。
在這個(gè)例子里,你期待的是NSRangeException ,但接收到的卻是NSGenericException 。
看起來你已經(jīng)做了一些研究!
示例項(xiàng)目地址:http://pan.baidu.com/s/1o6x6zxg
新聞熱點(diǎn)
疑難解答
圖片精選
網(wǎng)友關(guān)注