前言
維基百科對單元測試的定義如下:
在計算機程式設計中,單元測試(英語:Unit Testing)又稱為模組測試, 是針對程式模組(軟體設計的最小單位)來進行正確性檢驗的測試工作。程式單元是應用的最小可測試部件。
在過程化程式設計中,一個單元就是單個程式、函式、過程等;對於物件導向程式設計,最小單元就是方法,包括基類(超類)、抽象類、或者派生類(子類)中的方法。
根據不同場景,單元的定義也不一樣,通常我們將C語言的單個函式或者面嚮物件語言的單個類視作測試的單元。在使用單元測試的過程中,我們要知道這一點:
單元測試並不是為了證明程式碼的正確性,它只是一種用來幫助我們發現錯誤的手段
單元測試不是萬能藥,它確實能幫助我們找到大部分程式碼邏輯上的bug,同時,為了提高測試覆蓋率,這能逼迫我們對程式碼不斷進行重構,提高程式碼質量等。
內建單元測試框架
在Xcode4.x中整合了測試框架OCUnit
,根據測試的目的大致可以將單元測試分為這三類:
- 效能測試:測試程式碼執行花費的時間
- 邏輯測試:測試程式碼執行結果是否符合預期
- 非同步測試:測試多執行緒操作程式碼
在我們新建專案的時候,已經預設選擇建立單元測試的框架,除了Unit Tests
之外還有一個UI Tests
是iOS9推出的新特性,針對UI介面的單元測試框架。在建立專案之後,會自動生成一個appName+Tests
的資料夾目錄,下面存放著單元測試的檔案
一個標準的測試類檔案程式碼如下。其中setUp
會在每一個測試用例開始前呼叫,用來初始化相關資料;tearDown
在測試用例完成後呼叫,可以用來釋放變數等結尾操作;testPerformanceExample
中的會將方法中的block
程式碼耗費時長列印出來;最後的testExample
用來執行我們需要的測試操作,正常情況下,我們不使用這個方法,而是建立名為test+測試目的
的方法來完成我們需要的操作:
在每個測試用例方法的左側有個菱形的標記,點選這個標記可以單獨的執行這個測試方法。如果測試通過沒有發生任何斷言錯誤,那麼這個菱形就會變成綠色勾選狀態。使用快捷鍵command+U
直接依次呼叫所有的單元測試。另外,可以在左側的檔案欄中選中單元測試欄目,然後直觀的看到所有測試的結果。同樣的點選右側菱形位置的按鈕可以執行單個測試方法或者檔案:
另外,為了保證單元測試的正確性,我們應當保證測試用例中只存在一個類或者只發生一個類變數的屬性修改。下面是我們測試中常用的巨集定義
1 2 3 4 5 6 |
XCTAssertNotNil(a1, format…) 當a1不為nil時成立 XCTAssert(expression, format...) 當expression結果為YES成立 XCTAssertTrue(expression, format...) 當expression結果為YES成立; XCTAssertEqualObjects(a1, a2, format...) 判斷相等,當[a1 isEqualTo: a2]返回YES的時候成立 XCTAssertEqual(a1, a2, format...) 當a1==a2返回YES時成立 XCTAssertNotEqual(a1, a2, format...) 當a1!=a2返回YES時成立 |
邏輯測試
筆者新建了一個用以測試的model
類,該類提供了三個介面。需要注意的是,在邏輯測試的某個操作步驟前後,應該有對應的資料發生了改變,這樣才能夠方便我們進行測試:
1 2 3 4 5 6 7 8 9 10 11 12 |
@interface LXDTestsModel : NSObject @property (nonatomic, readonly, copy) NSString * name; @property (nonatomic, readonly, strong) NSNumber * age; @property (nonatomic, readonly, assign) NSUInteger flags; + (instancetype)modelWithName: (NSString *)name age: (NSNumber *)age flags: (NSUInteger)flags; - (instancetype)initWithDictionary: (NSDictionary *)dict; - (NSDictionary *)modelToDictionary; @end |
在測試用例中,我定義了一個testModelConvert
方法用來測試模型跟json之間的轉換是否正確:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
- (void)testModelConvert { NSString * json = @"{\"name\":\"SindriLin\",\"age\":22,\"flags\":987654321}"; NSMutableDictionary * dict = [[NSJSONSerialization JSONObjectWithData: [json dataUsingEncoding: NSUTF8StringEncoding] options: kNilOptions error: nil] mutableCopy]; LXDTestsModel * model = [[LXDTestsModel alloc] initWithDictionary: dict]; XCTAssertNotNil(model); XCTAssertTrue([model.name isEqualToString: @"SindriLin"]); XCTAssertTrue([model.age isEqual: @(22)]); XCTAssertEqual(model.flags, 987654321); XCTAssertTrue([model isKindOfClass: [LXDTestsModel class]]); model = [LXDTestsModel modelWithName: @"Tessie" age: dict[@"age"] flags: 562525]; XCTAssertNotNil(model); XCTAssertTrue([model.name isEqualToString: @"Tessie"]); XCTAssertTrue([model.age isEqual: dict[@"age"]]); XCTAssertEqual(model.flags, 562525); NSDictionary * modelJSON = [model modelToDictionary]; XCTAssertTrue([modelJSON isEqual: dict] == NO); dict[@"name"] = @"Tessie"; dict[@"flags"] = @(562525); XCTAssertTrue([modelJSON isEqual: dict]); } |
邏輯測試的目的是為了檢測在程式碼執行前後發生的變化是否符合預期,因此可以說80%左右
的單元測試都是邏輯測試。最開始筆者學習單元測試的時候總有一種無從下手的感覺,但是當你從無形抽象的邏輯操作找到了資料變化的規律的時候,對應的單元測試就能很快的寫出來了
效能測試
相較於上面的邏輯測試,效能測試的地位有些尷尬。在現今的開發環境下,我們已經能通過 instrument
工具很好的查詢到專案中的程式碼耗時點,效能測試就有種棄之可惜,食之無味
的感覺了。但是為了本文的完整性,還是將這個補充完畢。筆者在測試model
類中新增了類方法,用來隨機生成100個類例項物件,並且在每次建立物件後讓執行緒休眠一段時間來模擬耗時操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
+ (NSArray<LXDTestsModel *> *)randomModels { NSMutableArray * models = @[].mutableCopy; NSArray * names = @[ @"SindriLin", @"Bison", @"XiongZengHui", @"ZengChengChun", @"Tessie" ]; NSArray * ages = @[ @15, @20, @25, @30, @35 ]; NSArray * flags = @[ @123, @456, @789, @012, <a href="http://www.jobbole.com/members/234">@234</a> ]; for (NSUInteger idx = 0; idx < 100; idx++) { LXDTestsModel * model = [LXDTestsModel modelWithName: names[arc4random() % names.count] age: ages[arc4random() % ages.count] flags: [flags[arc4random() % flags.count] unsignedIntegerValue]]; [models addObject: model]; [NSThread sleepForTimeInterval: 0.01]; } return models; } |
執行測試用法後控制檯會輸出下面的資訊,其中紅框中表示執行程式碼總耗時,在此demo中總共執行了11.015秒
的時長
雖然效能測試的定位確實有些雞肋,但是另一方面,直接使用單元測試來獲取某段程式碼的執行時間要比使用instrument
快的多。通過效能測試直觀的獲取執行時間後,我們可以根據需要來決定是否將這些程式碼放到子執行緒中執行來優化程式碼(很多時候,資料轉換會佔用大量的CPU計算資源)
非同步測試
由於單元測試是在主執行緒中進行的,因此非同步操作的測試在執行完畢之前,往往已經結束了。為了實現非同步測試,筆者採用while()
的方式無限迴圈等待,為了實現這個效果,我在LXDTestsModel
標頭檔案中新增了一個NSData
型別的屬性以及一個非同步操作的介面方法,通過判斷這個屬性值來實現效果:
1 2 3 4 5 6 7 8 9 10 11 12 |
- (void)asyncConvertToData { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSDictionary * modelJSON = nil; for (NSInteger idx = 0; idx < 20; idx++) { modelJSON = [self modelToDictionary]; [self setValuesWithDictionary: modelJSON]; [NSThread sleepForTimeInterval: 0.001]; } _data = [NSJSONSerialization dataWithJSONObject: modelJSON options: NSJSONWritingPrettyPrinted error: nil]; }); } |
上面的程式碼在系統建立的預設等級的子執行緒中執行了一段耗時程式碼,最後把json轉換成NSData
資料儲存在自身的屬性中。對應的非同步測試程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
- (void)testAsync { NSDictionary * dict = @{ @"name": @"SindriLin", @"age": @22, @"flags": @987654321 }; LXDTestsModel * model = [[LXDTestsModel alloc] initWithDictionary: dict]; XCTAssertNotNil(model); [model asyncConvertToData]; while (model.data == nil) { CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.01, YES); NSLog(@"waiting"); } XCTAssertNotNil(model.data); NSLog(@"convert finish %@", model.data); } |
同樣的,如果你的非同步操作是網路請求,那麼在執行的回撥外對獲取的資料型別加上__block
修飾,然後判斷這個獲取的資料是否不為空來停止迴圈。另外最重要的是你必須在你的死迴圈中加入CFRunLoopRunInModel
這個函式的呼叫來保證即便是在等待的情況下,你的主執行緒仍然能處理其他的事情。
1 2 3 4 5 6 7 8 9 10 11 |
__block BOOL complete = NO; __block NSData * data = nil; [network POST: @"http://xxxxxxx" parameters: nil completion: ^(NSData * receiveData) { data = receiveData; complete = YES: }]; while (!complete) { CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.01, YES); NSLog(@"requesting"); } |
尾言
最開始筆者一度認為單元測試是個比較考驗技術的東西,但恰恰相反的,單元測試的使用與概念是相當簡單的一個東西,難點在於不知道怎麼用,這就需要我們持續的使用練習才能更好的服務於我們的開發。此外,常用的第三方框架例如YYModel
、AFNetworking
、Alamofire
等等優秀框架中也有對框架自身編寫的單元測試,學習仿寫這些單元測試也是快速提升自己的一種手段。
很多時候,我們的專案中難免發生多個類之間的互動處理,而這種操作非常的不好除錯。單元測試的原則之一就在於我們用來測試的程式碼要求功能很單一,這其實與良好的程式碼設計的思想是非常相符的。一方面來說,良好的程式碼結構設計可以讓我們的測試用例的構建更加快速簡單;反過來單元測試逼著我們去想辦法減少類之間的耦合以此來減少甚至排除測試的干擾。無論如何,如果你想成為更好的開發者,單元測試是我們快速提升程式碼認知的重要手段之一。