什么是單元測(cè)試 ?
針對(duì)程序模塊(軟件設(shè)計(jì)的最小單位)來(lái)進(jìn)行正確性檢驗(yàn)的測(cè)試工作娃善。程序單元是應(yīng)用的最小可測(cè)試部件多柑。對(duì)于面向?qū)ο缶幊棠淌牵钚卧褪?strong>方法
iOS 集成了自己的測(cè)試框架 OCUnit 和 UITests
為什么單元測(cè)試 ?
執(zhí)行單元測(cè)試,就是為了證明這段代碼的行為和我們期望的一致竣灌,比如測(cè)試一些功能是否正常聂沙,接口是否能正常,特別在一些大的項(xiàng)目初嘹,以防止程序被誤改或引起新的問(wèn)題 及汉。
單元測(cè)試還具有一下幾個(gè)好處:
- 協(xié)助程序員盡快找到BUG的具體位置
- 讓程序員對(duì)自己的程序更有自信
- 能夠讓程序員在提交項(xiàng)目之前就將代碼變的更加健壯
- 能夠協(xié)助程序員更好的進(jìn)行開(kāi)發(fā)
- 能夠向其他程序員展現(xiàn)你的程序該如何調(diào)用
- 能夠讓項(xiàng)目主管更了解系統(tǒng)的當(dāng)前狀況
如對(duì)其意義仍有疑惑請(qǐng)看這篇文章
單元測(cè)試 怎么寫 ?
我在剛寫單元測(cè)試,也看了網(wǎng)上有很多文章屯烦,但是依然無(wú)從下手坷随。本文總結(jié)了幾點(diǎn),并摘錄了一些例子驻龟。
-
** 什么該測(cè)試 **
- 項(xiàng)目中的公共類中的公開(kāi)方法温眉,將公共組件加入單元測(cè)試可以大大加強(qiáng)底層操作的正確性和健壯性。
- 網(wǎng)絡(luò)數(shù)據(jù)層方法的測(cè)試翁狐,數(shù)據(jù)接口一般不會(huì)太多类溢,這里的測(cè)試可以保證接口的正常。
- 業(yè)務(wù)邏輯層測(cè)試露懒,這樣可以讓業(yè)務(wù)邏輯保持正確闯冷,產(chǎn)品發(fā)布后可以直接通過(guò)業(yè)務(wù)邏輯的單元測(cè)試來(lái)找到BUG。
- 修復(fù)bug時(shí)測(cè)試懈词, 如用戶反映有個(gè)bug 窃躲,可以先寫個(gè)測(cè)試復(fù)現(xiàn)bug,接著找到問(wèn)題修復(fù)bug 钦睡,測(cè)試驗(yàn)證蒂窒,最后要保留測(cè)試躁倒,
更易于測(cè)試的代碼
很多時(shí)候,項(xiàng)目中難免發(fā)生多個(gè)類之間的交互處理, 耦合度高洒琢,而這種操作非常的不好調(diào)試秧秉。
單元測(cè)試的原則之一就在于我們用來(lái)測(cè)試的代碼要求功能很單一,這其實(shí)與良好的代碼設(shè)計(jì)的思想是非常相符的衰抑。
一方面來(lái)說(shuō)象迎,良好的代碼結(jié)構(gòu)設(shè)計(jì)可以讓我們的測(cè)試用例的構(gòu)建更加快速簡(jiǎn)單;反過(guò)來(lái)單元測(cè)試逼著我們?nèi)ハ朕k法減少類之間的耦合以此來(lái)減少甚至排除測(cè)試的干擾呛踊。無(wú)論如何砾淌,如果你想成為更好的開(kāi)發(fā)者,單元測(cè)試是我們快速提升代碼認(rèn)知的重要手段之一谭网。-
關(guān)注覆蓋率
單元測(cè)試寫的是否合理或者是否達(dá)到了要求的一個(gè)唯一的標(biāo)準(zhǔn)就是整個(gè)測(cè)試的代碼覆蓋率汪厨。代碼覆蓋率其實(shí)就是測(cè)試代碼所運(yùn)行到的實(shí)際程序路徑的覆蓋率。在實(shí)際程序中可能會(huì)有很多的循環(huán)愉择、判斷等分支路徑劫乱。一個(gè)好的單元測(cè)試應(yīng)該能夠?qū)⑺锌赡艿穆窂蕉紝⒆叩剑@樣就可以保證大多數(shù)情況都測(cè)試過(guò)了锥涕。邊界條件數(shù)據(jù)衷戈,比如值類型數(shù)據(jù)的最大值、最小值层坠、DBNull殖妇,或者是方法中所使用的條件邊界,例如a>100那么100就變成了這個(gè)數(shù)據(jù)的邊界破花。而且在測(cè)試的時(shí)候還必須把超出邊界的數(shù)據(jù)作為測(cè)試條件進(jìn)行測(cè)試谦趣。
空數(shù)據(jù),一般空數(shù)據(jù)對(duì)應(yīng)于引用類型的數(shù)據(jù)旧乞,也就是Null值蔚润。
** 格式不正確數(shù)據(jù)**,對(duì)于引用類型的數(shù)據(jù)或者結(jié)構(gòu)對(duì)象尺栖,類型雖然正確但是其內(nèi)部的數(shù)據(jù)結(jié)構(gòu)不正確的數(shù)據(jù)嫡纠。例如一個(gè)數(shù)據(jù)庫(kù)實(shí)體對(duì)象,數(shù)據(jù)庫(kù)中要求其某個(gè)屬性必須為非空延赌,但是這時(shí)我們可以屬于一個(gè)空除盏。這樣這個(gè)對(duì)象就屬于一個(gè)不正確數(shù)據(jù)庫(kù)。
在編寫單元測(cè)試代碼的時(shí)候先了解到被測(cè)試方法可能會(huì)使用的外部數(shù)據(jù)挫以,然后將這些外部數(shù)據(jù)一次設(shè)置為上面規(guī)定的這幾種情況者蠕,然后再執(zhí)行方法。這樣就基本可以達(dá)到外部數(shù)據(jù)所有情況都能夠正確測(cè)試到了掐松。
摘錄的一些例子
** 例1**
在邏輯測(cè)試的某個(gè)操作步驟前后踱侣,應(yīng)該有對(duì)應(yīng)的數(shù)據(jù)發(fā)生了改變粪小,這樣才能夠方便我們進(jìn)行測(cè)試:
@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
在測(cè)試用例中,我定義了一個(gè)testModelConvert
方法用來(lái)測(cè)試模型跟json之間的轉(zhuǎn)換是否正確:
- (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]);
}
** 例二 **
由于單元測(cè)試是在主線程中進(jìn)行的抡句,因此異步操作的測(cè)試在執(zhí)行完畢之前探膊,往往已經(jīng)結(jié)束了。有兩種方法解決
- 采用while()的方式無(wú)限循環(huán)等待
//waitForExpectationsWithTimeout是等待時(shí)間待榔,超過(guò)了就不再等待往下執(zhí)行逞壁。
#define WAIT do {
[self expectationForNotification:@"RSBaseTest" object:nil handler:nil];
[self waitForExpectationsWithTimeout:30 handler:nil];
} while (0)
#define NOTIFY [[NSNotificationCenter defaultCenter]postNotificationName:@"RSBaseTest" object:nil]
-(void)testRequest{
// 1.獲得請(qǐng)求管理者
AFHTTPRequestOperationManager *mgr = [AFHTTPRequestOperationManager manager];
mgr.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:@"text/html",nil];
// 2.發(fā)送GET請(qǐng)求
[mgr GET:@"http://www.weather.com.cn/adat/sk/101110101.html" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(@"responseObject:%@",responseObject);
XCTAssertNotNil(responseObject, @"返回出錯(cuò)");
NOTIFY //繼續(xù)執(zhí)行
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(@"error:%@",error);
XCTAssertNil(error, @"請(qǐng)求出錯(cuò)");
NOTIFY //繼續(xù)執(zhí)行
}];
WAIT //暫停
}
- 另一種異步測(cè)試實(shí)現(xiàn) 使用GCD信號(hào)量
- (void)downloadImageURLWithString:(NSString *)URLString
{ dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
NSURL *url = [NSURL URLWithString:URLString];
__unused Photo *photo = [[Photo alloc] initwithURL:url
withCompletionBlock:^(UIImage *image, NSError *error) {
if (error) {
XCTFail(@"%@ failed. %@", URLString, error);
}
dispatch_semaphore_signal(semaphore);
}];
dispatch_time_t timeoutTime = dispatch_time(DISPATCH_TIME_NOW, kDefaultTimeoutLengthInNanoSeconds);
if (dispatch_semaphore_wait(semaphore, timeoutTime)) {
XCTFail(@"%@ timed out", URLString);
}
}
** 借鑒 開(kāi)源項(xiàng)目**
常用的第三方框架例如YYModel、AFNetworking锐锣、Alamofire等等優(yōu)秀框架中也有對(duì)框架自身編寫的單元測(cè)試腌闯,學(xué)習(xí)仿寫這些單元測(cè)試也是快速提升自己的一種手段。