一、單元測試的定義
在計算機編程中,單元測試(英語:Unit Testing)又稱為模塊測試, 是針對程序模塊(軟件設(shè)計的最小單位)來進行正確性檢驗的測試工作。程序單元是應(yīng)用的最小可測試部件。
在過程化編程中,一個單元就是單個程序、函數(shù)、過程等;對于面向?qū)ο缶幊?,最小單元就是方法,包括基類(超類)、抽象類、或者派生類(子類)中的方法?/p>
根據(jù)不同場景,單元的定義也不一樣,通常我們將C語言的單個函數(shù)或者面向?qū)ο笳Z言的單個類視作測試的單元。在使用單元測試的過程中,我們要知道這一點:
單元測試并不是為了證明代碼的正確性,它只是一種用來幫助我們發(fā)現(xiàn)錯誤的手段
單元測試不是萬能藥,它確實能幫助我們找到大部分代碼邏輯上的bug,同時,為了提高測試覆蓋率,這能逼迫我們對代碼不斷進行重構(gòu),提高代碼質(zhì)量等。
二、iOS單元測試
xcode本身的測試框架集成:在Xcode4.x中集成了測試框架OCUnit,UI Tests是iOS9推出的新特性。目前我們在創(chuàng)建項目的時候會默認選中有關(guān)測試的這兩項:Include Unit Tests、Include UI Tests。在創(chuàng)建項目之后,會自動生成一個appName+Tests的文件夾目錄,下面存放著單元測試的文件。
根據(jù)測試的目的大致可以將單元測試分為這三類:
a.性能測試:測試代碼執(zhí)行花費的時間
b.邏輯測試:測試代碼執(zhí)行結(jié)果是否符合預(yù)期
c.異步測試:測試多線程操作代碼
UnitTest文件里面方法介紹:
- (void)setUp {//每一個測試用例開始前調(diào)用,用來初始化相關(guān)數(shù)據(jù)
[super setUp];
// Put setup code here. This method is called before the invocation of each test method in the class.
}
- (void)tearDown {//測試用例完成后調(diào)用,可以用來釋放變量等結(jié)尾操作
// Put teardown code here. This method is called after the invocation of each test method in the class.
[super tearDown];
}
\- (void)testExample {//用來執(zhí)行我們需要的測試操作,正常情況下,我們不使用這個方法,而是創(chuàng)建名為test+測試目的的方法來完成我們需要的操作(注意:此時自定義的方法需要以test開頭方能進行測試,否則左邊是不顯示菱形的)
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
}
\- (void)testPerformanceExample {//會將方法中的block代碼耗費時長打印出來--默認執(zhí)行了10次,打印出了平均耗時,和各次的耗時,最大誤差不超過10%。其中運行之后block這行右側(cè)顯示的就是平均耗時。
// This is an example of a performance test case.
[self measureBlock:^{
// Put the code you want to measure the time of here.
}];
}
在每個測試用例方法的左側(cè)有個菱形的標記,點擊這個標記可以單獨的運行這個測試方法。如果測試通過沒有發(fā)生任何斷言錯誤,那么這個菱形就會變成綠色勾選狀態(tài)。使用快捷鍵command+U直接依次調(diào)用所有的單元測試。
另外,可以在左側(cè)的文件欄中選中單元測試欄目,然后直觀的看到所有測試的結(jié)果。同樣的點擊右側(cè)菱形位置的按鈕可以運行單個測試方法或者文件:

為了保證單元測試的正確性,我們應(yīng)當保證測試用例中只存在一個類或者只發(fā)生一個類變量的屬性修改。下面是我們測試中常用的宏定義:(XCTest 帶有許多內(nèi)建的斷言)
XCTAssertNotNil(a1, format…) 當a1不為nil時成立
XCTAssert(expression, format...) 當expression結(jié)果為YES成立
XCTAssertTrue(expression, format...) 當expression結(jié)果為YES成立;
XCTAssertEqualObjects(a1, a2, format...) 判斷相等,當[a1 isEqualTo: a2]返回YES的時候成立
XCTAssertEqual(a1, a2, format...) 當a1==a2返回YES時成立
XCTAssertNotEqual(a1, a2, format...) 當a1!=a2返回YES時成立
</br>
&&
XCTFail(format…) 生成一個失敗的測試;
XCTAssertNil(a1, format...)為空判斷,a1為空時通過,反之不通過;
XCTAssertNotNil(a1, format…)不為空判斷,a1不為空時通過,反之不通過;
XCTAssert(expression, format...)當expression求值為TRUE時通過;
XCTAssertTrue(expression, format...)當expression求值為TRUE時通過;
XCTAssertFalse(expression, format...)當expression求值為False時通過;
XCTAssertEqualObjects(a1, a2, format...)判斷相等,[a1 isEqual:a2]值為TRUE時通過,其中一個不為空時,不通過;
XCTAssertNotEqualObjects(a1, a2, format...)判斷不等,[a1 isEqual:a2]值為False時通過;
XCTAssertEqual(a1, a2, format...)判斷相等(當a1和a2是 C語言標量、結(jié)構(gòu)體或聯(lián)合體時使用, 判斷的是變量的地址,如果地址相同則返回TRUE,否則返回NO);
XCTAssertNotEqual(a1, a2, format...)判斷不等(當a1和a2是 C語言標量、結(jié)構(gòu)體或聯(lián)合體時使用);
XCTAssertEqualWithAccuracy(a1, a2, accuracy, format...)判斷相等,(double或float類型)提供一個誤差范圍,當在誤差范圍(+/-accuracy)以內(nèi)相等時通過測試;
XCTAssertNotEqualWithAccuracy(a1, a2, accuracy, format...) 判斷不等,(double或float類型)提供一個誤差范圍,當在誤差范圍以內(nèi)不等時通過測試;
XCTAssertThrows(expression, format...)異常測試,當expression發(fā)生異常時通過;反之不通過;(很變態(tài)) XCTAssertThrowsSpecific(expression, specificException, format...) 異常測試,當expression發(fā)生specificException異常時通過;反之發(fā)生其他異?;虿话l(fā)生異常均不通過;
XCTAssertThrowsSpecificNamed(expression, specificException, exception_name, format...)異常測試,當expression發(fā)生具體異常、具體異常名稱的異常時通過測試,反之不通過;
XCTAssertNoThrow(expression, format…)異常測試,當expression沒有發(fā)生異常時通過測試;
XCTAssertNoThrowSpecific(expression, specificException, format...)異常測試,當expression沒有發(fā)生具體異常、具體異常名稱的異常時通過測試,反之不通過;
XCTAssertNoThrowSpecificNamed(expression, specificException, exception_name, format...)異常測試,當expression沒有發(fā)生具體異常、具體異常名稱的異常時通過測試,反之不通過
三、測試
1、邏輯測試:邏輯測試的目的是為了檢測在代碼執(zhí)行前后發(fā)生的變化是否符合預(yù)期
e.g:
@interface TestModel1 : NSObject
@property (nonatomic, copy) NSString * name;
@property (nonatomic, strong) NSNumber * age;
@property (nonatomic, assign) NSUInteger flags;
+ (instancetype)modelWithName: (NSString *)name age: (NSNumber *)age flags: (NSUInteger)flags;
- (instancetype)initWithDictionary: (NSDictionary *)dict;
- (NSDictionary *)modelToDictionary;
@end
@implementation TestModel1
+ (instancetype)modelWithName:(NSString *)name age:(NSNumber *)age flags:(NSUInteger)flags
{
TestModel1 *model = [[self alloc] init];
model.name = name;
model.age = age;
model.flags = flags;
return model;
}
- (instancetype)initWithDictionary: (NSDictionary *)dict
{
self.name = dict[@"name"];
self.age = dict[@"age"];
self.flags = [dict[@"flags"] integerValue];
return self;
}
- (NSDictionary *)modelToDictionary
{
return @{@"name":self.name,@"age":self.age,@"flags":[NSNumber numberWithInteger:self.flags]};
}
@end
然后在測試文件里面:
\- (void)testModelConvert
{
NSString * json = @"{\"name\":\"SindriLin\",\"age\":22,\"flags\":987654321}";
NSMutableDictionary * dict = [[NSJSONSerialization JSONObjectWithData: [json dataUsingEncoding: NSUTF8StringEncoding] options: kNilOptions error: nil] mutableCopy];
TestModel1 * model = [[TestModel1 alloc] initWithDictionary: dict];
XCTAssertNotNil(model);
XCTAssertTrue([model.name isEqualToString: @"SindriLin"]);
XCTAssertTrue([model.age isEqual: @(22)]);
XCTAssertEqual(model.flags, 987654321);
XCTAssertTrue([model isKindOfClass: [TestModel1 class]]);
model = [TestModel1 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]);
}
2、性能測試:
在平常的工作中,我們還可以通過: instrument(xcode->product->profile)工具很好的查找到項目中的代碼耗時點,(后面介紹)。先介紹單元測試的性能測試:
測試文件:
\- (void)testPerformanceExample {//會將方法中的block代碼耗費時長打印出來--默認執(zhí)行了10次,打印出了平均耗時,和各次的耗時,最大誤差不超過10%。
// This is an example of a performance test case.
[self measureBlock:^{
// Put the code you want to measure the time of here.
[TestModel1 randomModels];
// for (int i = 0; i<100; ++i) {
// NSLog(@"wgj:%d",i);
// }
}];
}
自定義的model文件中添加:
\+ (NSArray<TestModel1 *> *)randomModels
{
NSMutableArray * models = @[].mutableCopy;
NSArray * names = @[
@"xiaoli01", @"xiaoli02", @"xiaoli03", @"xiaoli04", @"xiaoli05"
];
NSArray * ages = @[
@15, @20, @25, @30, @35
];
NSArray * flags = @[
@123, @456, @789, @012, @234
];
for (NSUInteger idx = 0; idx < 100; idx++) {
TestModel1 * model = [self 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;
}

在平常的test方法中,也會打印測試方法的執(zhí)行時間,例如下面,但是沒有上面這種在性能測試方法中測的準確。

打印臺會打印各測試方法的耗時,直接使用單元測試來獲取某段代碼的執(zhí)行時間要比使用instrument快的多(instrument定位更精確)。通過性能測試直觀的獲取執(zhí)行時間后,我們可以根據(jù)需要來決定是否將這些代碼放到子線程中執(zhí)行來優(yōu)化代碼(很多時候,數(shù)據(jù)轉(zhuǎn)換會占用大量的CPU計算資源)
3.異步測試
在Xcode 6之前的版本里面并沒有內(nèi)置XCTest,想使用Xcode測試的只能是在主線程的RunLoop里面使用一個while循環(huán),然后一直等待響應(yīng)或者直到timeout.(Xcode 6中添加了新特性:XCTestExpectation 和性能測試:特性是內(nèi)建的對于異步測試的支持,測試能夠為了確定的合適的條件等待一個指定時間長度,而不需要求助于GCD)
e.g.老方法:
\- (void)testAsync
{// 異步測試
NSDictionary * dict = @{
@"name": @"MrLi",
@"age": @28,
@"flags": @987
};
TestModel1 * model = [[TestModel1 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);
}
model文件:
\- (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 setValuesForKeysWithDictionary: modelJSON];
[NSThread sleepForTimeInterval: 0.001];
}
_data = [NSJSONSerialization dataWithJSONObject: modelJSON options: NSJSONWritingPrettyPrinted error: nil];
});
}
e.g.新方法
在Xcode 6里,蘋果以XCTestExpection類的方式向XCTest框架里添加了測試期望(test expection)。當我們實例化一個測試期望(XCTestExpectation)的時候,測試框架就會預(yù)計它在之后的某一時刻被實現(xiàn)。最終的程序完成代碼塊中的測試代碼會調(diào)用XCTestExpection類中的fulfill方法來實現(xiàn)期望。
我們讓測試框架等待(有時限)測試期望通過XCTestCase的waitForExpectationsWithTimeout:handler:方法實現(xiàn)。如果完成處理的代碼在指定時限里執(zhí)行并調(diào)用了fulfill方法,那么就說明所有的測試期望在此期間都已經(jīng)被實現(xiàn)。此方法中的handler的參數(shù)其實是一個block,block中若是寫有代碼,代碼執(zhí)行的條件(滿足其中之一就可執(zhí)行):a、所有期望在指定的時間內(nèi)都以實現(xiàn); b、期望在指定的時間內(nèi)沒有實現(xiàn)(此時會報錯,但是block里面的方法會執(zhí)行)。
代碼:
<pre>
- (void)testAsyncOutTime{
XCTestExpectation *ex = [self expectationWithDescription:@"wgj001"];
NSURL *url = [NSURL URLWithString:@"https://www.baidu.com"];
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSURLSessionTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSLog(@"url請求完成");
[ex fulfill];//如果完成處理的代碼在指定時限里執(zhí)行并調(diào)用了fulfill方法,那么就說明所有的測試期望在此期間都已經(jīng)被實現(xiàn)
}];
[task resume];
[self waitForExpectationsWithTimeout:1 handler:^(NSError * _Nullable error) {
if (error) {
NSLog(@"wati:%@",error);
}
// [task cancel];
NSLog(@"url請求超時-結(jié)束");
}];
}
</pre>
斷言(詳解):
<pre>
1、XCTFail(...):參數(shù)可有可無,若有則須是字符串,參數(shù)為錯誤的描述。無條件的都是測試失敗。在測試驅(qū)動里有這么個情況,你定義了測試方法,但是沒有給出具體的實現(xiàn)。那么你不會希望這個測試能通過的。一般被用作一個占位斷言。等你的測試方法完善好了之后再換成最貼近你的測試的斷言。有或者,在某些情況下else了之后就是不應(yīng)該出現(xiàn)的情況。那么這個時候可以把XCTFail放在這個else里面。
2、XCTAssertNil(expression, ...)/XCTAssertNotNil(expression, ...):判斷給定的表達式值是否為nil, XCTAssertNil(表達式為nil的時候通過),XCTAssertNotNil(表達式不為nil的時候通過),其中...是錯誤描述,為字符串類型,下面的表達式中的意思都是一樣的。
3、XCTAssert(expression, ...):如果expression(表達式)執(zhí)行的結(jié)果為true的話,這個測試通過。否則,測試失敗,并在console中輸出后面的format字符串.
4、后面基于XCTAssert演化出來的斷言,不僅可以滿足測試的需求而且可以更好更明確的表達出你要測試的是什么。最好是使用這些演化出來的斷言:
a. Bool測試
對于bool型的數(shù)據(jù),或者只是簡單的bool型的表達式,使用XCTestAssertTrue或者XCTestAssertFalse:
XCTAssertTrue(expression, format...)
XCTAssertFalse(expression, format...)
b. 相等測試
測試兩個數(shù)值的值是否相等使用XCTAssert[Not]Equal:
XCTAssertEqual(expression1, expression2, format...)
XCTAssertNotEqual(expression1, expression2, format...);
判斷兩個對象用:XCTAssertEqualObjects(expression1, expression2, ...)和XCTAssertNotEqualObjects(expression1, expression2, ...)
在Double、Float型數(shù)據(jù)的對比中使用XCTAssert[Not]EqualWithAccuracy來處理浮點精度的問題:
XCTAssertEqualWithAccuracy(expression1, expression2, accuracy, format...)
XCTAssertNotEqualWithAccuracy(expression1, expression2, accuracy, format...)
e.g. XCTAssertEqualWithAccuracy(12, 14, 1,@"wgj"),則不通過,因為12和14的差別已經(jīng)超過了設(shè)定的值1。
XCTAssertGreaterThan[OrEqual] & XCTAssertLessThan[OrEqual], 和下面的條件操作符比較的是一個意思 == with >, >=, <, 以及 <=
5、拋異常:
a.
XCTAssertThrows(expression, ...):表達式拋異常時,通過;反之,不通過。e.g. XCTAssertThrows([model onlyTest],@"wgj01");方法在model中只有聲明但沒有實現(xiàn),此表達式是會異常的,但是這句測試的代碼則是通過的。
b.
XCTAssertThrowsSpecific(expression, exception_class, ...):表達式 拋出異常,并且拋出的異常類屬于NSException,才會執(zhí)行通過;反之。
e.g.
XCTAssertThrowsSpecific([model onlyTest02],NSException,@"wgj001");--onlyTest02方法實現(xiàn)時:運行崩潰;onlyTest02方法未實現(xiàn)時,執(zhí)行未實現(xiàn)的方法,系統(tǒng)會自動生成NSException類型的異常,符合定義的NSException類,測試代碼運行通過。
c.
XCTAssertThrowsSpecificNamed(expression, exception_class, exception_name, ...):表達式拋出異常,并且拋出的異常類屬于NSException,并且異常類的名字符合定義的名字時,才會執(zhí)行通過;反之。
e.g.
XCTAssertThrowsSpecificNamed([model onlyExceptionTest], NSException, @"自定義異常",@"wgj002");
model中的實現(xiàn)方法:
-
(void)onlyExceptionTest{
NSException *exx = [NSException exceptionWithName:@"自定義異常" reason:@"崩潰test02" userInfo:@{@"key02":@"value02"}];@throw exx;
}
此種,測試代碼是通過的。
若改為:
XCTAssertThrowsSpecificNamed([model onlyExceptionTest], NSException, @"隨便寫的名字",@"wgj002");則測試代碼不通過,因為異常名字不匹配。
</pre>