一、單元測試的定義
在計(jì)算機(jī)編程中,單元測試(英語:Unit Testing)又稱為模塊測試, 是針對程序模塊(軟件設(shè)計(jì)的最小單位)來進(jìn)行正確性檢驗(yàn)的測試工作。程序單元是應(yīng)用的最小可測試部件。
在過程化編程中褂傀,一個單元就是單個程序、函數(shù)加勤、過程等仙辟;對于面向?qū)ο缶幊蹋钚卧褪欠椒罚ɑ悾ǔ悾┑⒊橄箢悺⒒蛘吲缮悾ㄗ宇悾┲械姆椒ā?/p>
根據(jù)不同場景戴尸,單元的定義也不一樣粟焊,通常我們將C語言的單個函數(shù)或者面向?qū)ο笳Z言的單個類視作測試的單元。在使用單元測試的過程中孙蒙,我們要知道這一點(diǎn):
單元測試并不是為了證明代碼的正確性吆玖,它只是一種用來幫助我們發(fā)現(xiàn)錯誤的手段
單元測試不是萬能藥,它確實(shí)能幫助我們找到大部分代碼邏輯上的bug马篮,同時,為了提高測試覆蓋率怜奖,這能逼迫我們對代碼不斷進(jìn)行重構(gòu)浑测,提高代碼質(zhì)量等。
二歪玲、iOS單元測試
xcode本身的測試框架集成:在Xcode4.x中集成了測試框架OCUnit迁央,UI Tests是iOS9推出的新特性。目前我們在創(chuàng)建項(xiàng)目的時候會默認(rèn)選中有關(guān)測試的這兩項(xiàng):Include Unit Tests滥崩、Include UI Tests岖圈。在創(chuàng)建項(xiàng)目之后,會自動生成一個appName+Tests的文件夾目錄钙皮,下面存放著單元測試的文件蜂科。
根據(jù)測試的目的大致可以將單元測試分為這三類:
a.性能測試:測試代碼執(zhí)行花費(fèi)的時間
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開頭方能進(jìn)行測試,否則左邊是不顯示菱形的)
// 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代碼耗費(fèi)時長打印出來--默認(rèn)執(zhí)行了10次贡定,打印出了平均耗時赋访,和各次的耗時,最大誤差不超過10%缓待。其中運(yùn)行之后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è)有個菱形的標(biāo)記,點(diǎn)擊這個標(biāo)記可以單獨(dú)的運(yùn)行這個測試方法旋炒。如果測試通過沒有發(fā)生任何斷言錯誤步悠,那么這個菱形就會變成綠色勾選狀態(tài)。使用快捷鍵command+U直接依次調(diào)用所有的單元測試国葬。
另外贤徒,可以在左側(cè)的文件欄中選中單元測試欄目,然后直觀的看到所有測試的結(jié)果汇四。同樣的點(diǎn)擊右側(cè)菱形位置的按鈕可以運(yùn)行單個測試方法或者文件:
為了保證單元測試的正確性接奈,我們應(yīng)當(dāng)保證測試用例中只存在一個類或者只發(fā)生一個類變量的屬性修改。下面是我們測試中常用的宏定義:(XCTest 帶有許多內(nèi)建的斷言)
XCTAssertNotNil(a1, format…) 當(dāng)a1不為nil時成立
XCTAssert(expression, format...) 當(dāng)expression結(jié)果為YES成立
XCTAssertTrue(expression, format...) 當(dāng)expression結(jié)果為YES成立通孽;
XCTAssertEqualObjects(a1, a2, format...) 判斷相等序宦,當(dāng)[a1 isEqualTo: a2]返回YES的時候成立
XCTAssertEqual(a1, a2, format...) 當(dāng)a1==a2返回YES時成立
XCTAssertNotEqual(a1, a2, format...) 當(dāng)a1!=a2返回YES時成立
</br>
&&
XCTFail(format…) 生成一個失敗的測試;
XCTAssertNil(a1, format...)為空判斷背苦,a1為空時通過互捌,反之不通過;
XCTAssertNotNil(a1, format…)不為空判斷行剂,a1不為空時通過秕噪,反之不通過;
XCTAssert(expression, format...)當(dāng)expression求值為TRUE時通過厚宰;
XCTAssertTrue(expression, format...)當(dāng)expression求值為TRUE時通過腌巾;
XCTAssertFalse(expression, format...)當(dāng)expression求值為False時通過;
XCTAssertEqualObjects(a1, a2, format...)判斷相等铲觉,[a1 isEqual:a2]值為TRUE時通過澈蝙,其中一個不為空時,不通過撵幽;
XCTAssertNotEqualObjects(a1, a2, format...)判斷不等灯荧,[a1 isEqual:a2]值為False時通過;
XCTAssertEqual(a1, a2, format...)判斷相等(當(dāng)a1和a2是 C語言標(biāo)量盐杂、結(jié)構(gòu)體或聯(lián)合體時使用, 判斷的是變量的地址逗载,如果地址相同則返回TRUE哆窿,否則返回NO);
XCTAssertNotEqual(a1, a2, format...)判斷不等(當(dāng)a1和a2是 C語言標(biāo)量撕贞、結(jié)構(gòu)體或聯(lián)合體時使用)更耻;
XCTAssertEqualWithAccuracy(a1, a2, accuracy, format...)判斷相等,(double或float類型)提供一個誤差范圍捏膨,當(dāng)在誤差范圍(+/-accuracy)以內(nèi)相等時通過測試秧均;
XCTAssertNotEqualWithAccuracy(a1, a2, accuracy, format...) 判斷不等,(double或float類型)提供一個誤差范圍号涯,當(dāng)在誤差范圍以內(nèi)不等時通過測試目胡;
XCTAssertThrows(expression, format...)異常測試,當(dāng)expression發(fā)生異常時通過链快;反之不通過誉己;(很變態(tài)) XCTAssertThrowsSpecific(expression, specificException, format...) 異常測試,當(dāng)expression發(fā)生specificException異常時通過域蜗;反之發(fā)生其他異尘匏或不發(fā)生異常均不通過;
XCTAssertThrowsSpecificNamed(expression, specificException, exception_name, format...)異常測試霉祸,當(dāng)expression發(fā)生具體異常筑累、具體異常名稱的異常時通過測試,反之不通過丝蹭;
XCTAssertNoThrow(expression, format…)異常測試慢宗,當(dāng)expression沒有發(fā)生異常時通過測試;
XCTAssertNoThrowSpecific(expression, specificException, format...)異常測試奔穿,當(dāng)expression沒有發(fā)生具體異常镜沽、具體異常名稱的異常時通過測試,反之不通過贱田;
XCTAssertNoThrowSpecificNamed(expression, specificException, exception_name, format...)異常測試缅茉,當(dāng)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)工具很好的查找到項(xiàng)目中的代碼耗時點(diǎn)扶平,(后面介紹)帆离。先介紹單元測試的性能測試:
測試文件:
\- (void)testPerformanceExample {//會將方法中的block代碼耗費(fèi)時長打印出來--默認(rèn)執(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ǔn)確猜扮。
打印臺會打印各測試方法的耗時,直接使用單元測試來獲取某段代碼的執(zhí)行時間要比使用instrument快的多(instrument定位更精確)监婶。通過性能測試直觀的獲取執(zhí)行時間后旅赢,我們可以根據(jù)需要來決定是否將這些代碼放到子線程中執(zhí)行來優(yōu)化代碼(很多時候,數(shù)據(jù)轉(zhuǎn)換會占用大量的CPU計(jì)算資源)
3.異步測試
在Xcode 6之前的版本里面并沒有內(nèi)置XCTest惑惶,想使用Xcode測試的只能是在主線程的RunLoop里面使用一個while循環(huán),然后一直等待響應(yīng)或者直到timeout.(Xcode 6中添加了新特性:XCTestExpectation 和性能測試:特性是內(nèi)建的對于異步測試的支持煮盼,測試能夠?yàn)榱舜_定的合適的條件等待一個指定時間長度,而不需要求助于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)僵控。當(dāng)我們實(shí)例化一個測試期望(XCTestExpectation)的時候,測試框架就會預(yù)計(jì)它在之后的某一時刻被實(shí)現(xiàn)鱼冀。最終的程序完成代碼塊中的測試代碼會調(diào)用XCTestExpection類中的fulfill方法來實(shí)現(xiàn)期望报破。
我們讓測試框架等待(有時限)測試期望通過XCTestCase的waitForExpectationsWithTimeout:handler:方法實(shí)現(xiàn)。如果完成處理的代碼在指定時限里執(zhí)行并調(diào)用了fulfill方法千绪,那么就說明所有的測試期望在此期間都已經(jīng)被實(shí)現(xiàn)充易。此方法中的handler的參數(shù)其實(shí)是一個block,block中若是寫有代碼翘紊,代碼執(zhí)行的條件(滿足其中之一就可執(zhí)行):a蔽氨、所有期望在指定的時間內(nèi)都以實(shí)現(xiàn); b帆疟、期望在指定的時間內(nèi)沒有實(shí)現(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)被實(shí)現(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ū)動里有這么個情況柬脸,你定義了測試方法他去,但是沒有給出具體的實(shí)現(xiàn)。那么你不會希望這個測試能通過的倒堕。一般被用作一個占位斷言灾测。等你的測試方法完善好了之后再換成最貼近你的測試的斷言。有或者垦巴,在某些情況下else了之后就是不應(yīng)該出現(xiàn)的情況媳搪。那么這個時候可以把XCTFail放在這個else里面铭段。
2、XCTAssertNil(expression, ...)/XCTAssertNotNil(expression, ...):判斷給定的表達(dá)式值是否為nil, XCTAssertNil(表達(dá)式為nil的時候通過)秦爆,XCTAssertNotNil(表達(dá)式不為nil的時候通過),其中...是錯誤描述序愚,為字符串類型,下面的表達(dá)式中的意思都是一樣的等限。
3爸吮、XCTAssert(expression, ...):如果expression(表達(dá)式)執(zhí)行的結(jié)果為true的話,這個測試通過精刷。否則拗胜,測試失敗,并在console中輸出后面的format字符串.
4怒允、后面基于XCTAssert演化出來的斷言埂软,不僅可以滿足測試的需求而且可以更好更明確的表達(dá)出你要測試的是什么。最好是使用這些演化出來的斷言:
a. Bool測試
對于bool型的數(shù)據(jù)纫事,或者只是簡單的bool型的表達(dá)式勘畔,使用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來處理浮點(diǎn)精度的問題:
XCTAssertEqualWithAccuracy(expression1, expression2, accuracy, format...)
XCTAssertNotEqualWithAccuracy(expression1, expression2, accuracy, format...)
e.g. XCTAssertEqualWithAccuracy(12, 14, 1,@"wgj"),則不通過丽惶,因?yàn)?2和14的差別已經(jīng)超過了設(shè)定的值1炫七。
XCTAssertGreaterThan[OrEqual] & XCTAssertLessThan[OrEqual], 和下面的條件操作符比較的是一個意思 == with >, >=, <, 以及 <=
5、拋異常:
a.
XCTAssertThrows(expression, ...):表達(dá)式拋異常時钾唬,通過万哪;反之,不通過抡秆。e.g. XCTAssertThrows([model onlyTest],@"wgj01");方法在model中只有聲明但沒有實(shí)現(xiàn)奕巍,此表達(dá)式是會異常的,但是這句測試的代碼則是通過的儒士。
b.
XCTAssertThrowsSpecific(expression, exception_class, ...):表達(dá)式 拋出異常的止,并且拋出的異常類屬于NSException,才會執(zhí)行通過着撩;反之诅福。
e.g.
XCTAssertThrowsSpecific([model onlyTest02],NSException,@"wgj001");--onlyTest02方法實(shí)現(xiàn)時:運(yùn)行崩潰;onlyTest02方法未實(shí)現(xiàn)時拖叙,執(zhí)行未實(shí)現(xiàn)的方法氓润,系統(tǒng)會自動生成NSException類型的異常,符合定義的NSException類薯鳍,測試代碼運(yùn)行通過旺芽。
c.
XCTAssertThrowsSpecificNamed(expression, exception_class, exception_name, ...):表達(dá)式拋出異常,并且拋出的異常類屬于NSException,并且異常類的名字符合定義的名字時采章,才會執(zhí)行通過;反之壶辜。
e.g.
XCTAssertThrowsSpecificNamed([model onlyExceptionTest], NSException, @"自定義異常",@"wgj002");
model中的實(shí)現(xiàn)方法:
-
(void)onlyExceptionTest{
NSException *exx = [NSException exceptionWithName:@"自定義異常" reason:@"崩潰test02" userInfo:@{@"key02":@"value02"}];@throw exx;
}
此種悯舟,測試代碼是通過的。
若改為:
XCTAssertThrowsSpecificNamed([model onlyExceptionTest], NSException, @"隨便寫的名字",@"wgj002");則測試代碼不通過砸民,因?yàn)楫惓C植黄ヅ洹?br>
</pre>