前言
維基百科對單元測試的定義如下:
在計算機程式設計中,單元測試(英語:Unit Testing)又稱為子產品測試, 是針對程式子產品(軟體設計的最小機關)來進行正确性檢驗的測試工作。程式單元是應用的最小可測試部件。
在過程化程式設計中,一個單元就是單個程式、函數、過程等;對于面向對象程式設計,最小單元就是方法,包括基類(超類)、抽象類、或者派生類(子類)中的方法。
根據不同場景,單元的定義也不一樣,通常我們将C語言的單個函數或者面向對象語言的單個類視作測試的單元。在使用單元測試的過程中,我們要知道這一點:
單元測試并不是為了證明代碼的正确性,它隻是一種用來幫助我們發現錯誤的手段
單元測試不是萬能藥,它确實能幫助我們找到大部分代碼邏輯上的bug,同時,為了提高測試覆寫率,這能逼迫我們對代碼不斷進行重構,提高代碼品質等。
内置單元測試架構
在Xcode4.x中內建了測試架構
OCUnit
,根據測試的目的大緻可以将單元測試分為這三類:
- 性能測試:測試代碼執行花費的時間
- 邏輯測試:測試代碼執行結果是否符合預期
- 異步測試:測試多線程操作代碼
在我們建立項目的時候,已經預設選擇建立單元測試的架構,除了
Unit Tests
之外還有一個
UI Tests
是iOS9推出的新特性,針對UI界面的單元測試架構。在建立項目之後,會自動生成一個
appName+Tests
的檔案夾目錄,下面存放着單元測試的檔案
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsICM0ITMvw1dvwlMvwlM3VWaWV2Zh1WaDdTJwlmc0N3LcRnbllmcv1yb0VXYvwlMyd2bNV2Zh1Wa-cmbw5COidDNhVzYmFGZmVjNkBjYtQjN4MDO38CXzV2Zh1WafRWYvxGc19CXvlmL1h2cuFWaq5ycldWYtlWLkF2bsBXdvw1LcpDc0RHaiojIsJye.png)
一個标準的測試類檔案代碼如下。其中
setUp
會在每一個測試用例開始前調用,用來初始化相關資料;
tearDown
在測試用例完成後調用,可以用來釋放變量等結尾操作;
testPerformanceExample
中的會将方法中的
block
代碼耗費時長列印出來;最後的
testExample
用來執行我們需要的測試操作,正常情況下,我們不使用這個方法,而是建立名為
test+測試目的
的方法來完成我們需要的操作:
測試用例
在每個測試用例方法的左側有個菱形的标記,點選這個标記可以單獨的運作這個測試方法。如果測試通過沒有發生任何斷言錯誤,那麼這個菱形就會變成綠色勾選狀态。使用快捷鍵
command+U
直接依次調用所有的單元測試。另外,可以在左側的檔案欄中選中單元測試欄目,然後直覺的看到所有測試的結果。同樣的點選右側菱形位置的按鈕可以運作單個測試方法或者檔案:
單元測試總覽
另外,為了保證單元測試的正确性,我們應當保證測試用例中隻存在一個類或者隻發生一個類變量的屬性修改。下面是我們測試中常用的宏定義
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
類,該類提供了三個接口。需要注意的是,在邏輯測試的某個操作步驟前後,應該有對應的資料發生了改變,這樣才能夠友善我們進行測試:
@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之間的轉換是否正确:
- (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: @()]);
XCTAssertEqual(model.flags, );
XCTAssertTrue([model isKindOfClass: [LXDTestsModel class]]);
model = [LXDTestsModel modelWithName: @"Tessie" age: dict[@"age"] flags: ];
XCTAssertNotNil(model);
XCTAssertTrue([model.name isEqualToString: @"Tessie"]);
XCTAssertTrue([model.age isEqual: dict[@"age"]]);
XCTAssertEqual(model.flags, );
NSDictionary * modelJSON = [model modelToDictionary];
XCTAssertTrue([modelJSON isEqual: dict] == NO);
dict[@"name"] = @"Tessie";
dict[@"flags"] = @();
XCTAssertTrue([modelJSON isEqual: dict]);
}
邏輯測試的目的是為了檢測在代碼執行前後發生的變化是否符合預期,是以可以說
80%左右
的單元測試都是邏輯測試。最開始筆者學習單元測試的時候總有一種無從下手的感覺,但是當你從無形抽象的邏輯操作找到了資料變化的規律的時候,對應的單元測試就能很快的寫出來了
性能測試
相較于上面的邏輯測試,性能測試的地位有些尴尬。在現今的開發環境下,我們已經能通過
instrument
工具很好的查找到項目中的代碼耗時點,性能測試就有種
棄之可惜,食之無味
的感覺了。但是為了本文的完整性,還是将這個補充完畢。筆者在測試
model
類中添加了類方法,用來随機生成100個類執行個體對象,并且在每次建立對象後讓線程休眠一段時間來模拟耗時操作:
+ (NSArray<LXDTestsModel *> *)randomModels
{
NSMutableArray * models = @[].mutableCopy;
NSArray * names = @[
@"SindriLin", @"Bison", @"XiongZengHui", @"ZengChengChun", @"Tessie"
];
NSArray * ages = @[
@, @, @, @, @
];
NSArray * flags = @[
@, @, @, @, @
];
for (NSUInteger idx = ; idx < ; 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: ];
}
return models;
}
運作測試用法後控制台會輸出下面的資訊,其中紅框中表示執行代碼總耗時,在此demo中總共運作了
11.015秒
的時長
性能測試輸出
雖然性能測試的定位确實有些雞肋,但是另一方面,直接使用單元測試來擷取某段代碼的執行時間要比使用
instrument
快的多。通過性能測試直覺的擷取執行時間後,我們可以根據需要來決定是否将這些代碼放到子線程中執行來優化代碼(很多時候,資料轉換會占用大量的CPU計算資源)
異步測試
由于單元測試是在主線程中進行的,是以異步操作的測試在執行完畢之前,往往已經結束了。為了實作異步測試,筆者采用
while()
的方式無限循環等待,為了實作這個效果,我在
LXDTestsModel
頭檔案中添加了一個
NSData
類型的屬性以及一個異步操作的接口方法,通過判斷這個屬性值來實作效果:
- (void)asyncConvertToData
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, ), ^{
NSDictionary * modelJSON = nil;
for (NSInteger idx = ; idx < ; idx++) {
modelJSON = [self modelToDictionary];
[self setValuesWithDictionary: modelJSON];
[NSThread sleepForTimeInterval: ];
}
_data = [NSJSONSerialization dataWithJSONObject: modelJSON options: NSJSONWritingPrettyPrinted error: nil];
});
}
上面的代碼在系統建立的預設等級的子線程中執行了一段耗時代碼,最後把json轉換成
NSData
資料儲存在自身的屬性中。對應的異步測試代碼如下:
- (void)testAsync
{
NSDictionary * dict = @{
@"name": @"SindriLin",
@"age": @,
@"flags": @
};
LXDTestsModel * model = [[LXDTestsModel alloc] initWithDictionary: dict];
XCTAssertNotNil(model);
[model asyncConvertToData];
while (model.data == nil) {
CFRunLoopRunInMode(kCFRunLoopDefaultMode, , YES);
NSLog(@"waiting");
}
XCTAssertNotNil(model.data);
NSLog(@"convert finish %@", model.data);
}
同樣的,如果你的異步操作是網絡請求,那麼在執行的回調外對擷取的資料類型加上
__block
修飾,然後判斷這個擷取的資料是否不為空來停止循環。另外最重要的是你必須在你的死循環中加入
CFRunLoopRunInModel
這個函數的調用來保證即便是在等待的情況下,你的主線程仍然能處理其他的事情。
__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, , YES);
NSLog(@"requesting");
}
尾言
最開始筆者一度認為單元測試是個比較考驗技術的東西,但恰恰相反的,單元測試的使用與概念是相當簡單的一個東西,難點在于不知道怎麼用,這就需要我們持續的使用練習才能更好的服務于我們的開發。此外,常用的第三方架構例如
YYModel
、
AFNetworking
、
Alamofire
等等優秀架構中也有對架構自身編寫的單元測試,學習仿寫這些單元測試也是快速提升自己的一種手段。
很多時候,我們的項目中難免發生多個類之間的互動處理,而這種操作非常的不好調試。單元測試的原則之一就在于我們用來測試的代碼要求功能很單一,這其實與良好的代碼設計的思想是非常相符的。一方面來說,良好的代碼結構設計可以讓我們的測試用例的建構更加快速簡單;反過來單元測試逼着我們去想辦法減少類之間的耦合以此來減少甚至排除測試的幹擾。無論如何,如果你想成為更好的開發者,單元測試是我們快速提升代碼認知的重要手段之一。