關(guān)于單元測(cè)試
在計(jì)算機(jī)編程中俏蛮,單元測(cè)試(英語(yǔ):Unit Testing)又稱為模塊測(cè)試, 是針對(duì)程序模塊的最小單位來進(jìn)行正確性檢驗(yàn)的測(cè)試工作瘦材。程序單元是應(yīng)用的最小可測(cè)試部件。在過程化編程中拷恨,一個(gè)單元就是單個(gè)程序超陆、函數(shù)、過程等喇澡;對(duì)于面向?qū)ο缶幊萄刚ぃ钚卧褪欠椒ǎɑ悾ǔ悾┣缇痢⒊橄箢惗链妗⒒蛘吲缮悾ㄗ宇悾┲械姆椒ā?-- 維基百科
《你應(yīng)該知道的單元測(cè)試》這篇文章對(duì)單元測(cè)試的基礎(chǔ)思想做了很好的總結(jié)。
ObjC 中國(guó)的期刊在第15期也討論了“測(cè)試”這個(gè)專題呕屎。
XCTest
XCTest
是蘋果公司提供的一個(gè)非常簡(jiǎn)單并且直接集成在 Xcode 中的測(cè)試框架让簿。當(dāng)工程創(chuàng)建時(shí),Xcode 會(huì)自動(dòng)為我們創(chuàng)建一個(gè)名為ProjectNameTests
的路徑并添加一個(gè)測(cè)試用例模板文件ProjectNameTests.m
(如果創(chuàng)建時(shí)未添加秀睛,之后可以通過添加 target 的方式增加測(cè)試 bundle)尔当。通過這個(gè)模板文件,我們可以了解XCTest
框架的使用方法蹂安。
#import <XCTest/XCTest.h>
@interface UnitTestDemoTests : XCTestCase
@end
@implementation UnitTestDemoTests
- (void)setUp {
[super setUp];
// Put setup code here. This method is called before the invocation of each test method in the class.
}
- (void)tearDown {
// Put teardown code here. This method is called after the invocation of each test method in the class.
[super tearDown];
}
- (void)testExample {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
}
- (void)testPerformanceExample {
// This is an example of a performance test case.
[self measureBlock:^{
// Put the code you want to measure the time of here.
}];
}
@end
首先椭迎,我們的測(cè)試用例類要繼承自XCTestCase
類锐帜,其中[setUp]
方法會(huì)在每個(gè)測(cè)試方法前執(zhí)行,而[tearDown]
方法會(huì)在每個(gè)測(cè)試方法后執(zhí)行畜号,真正的測(cè)試方法必須以 testXXX 的格式命名缴阎,且不能有參數(shù)。
測(cè)試時(shí)弄兜,快捷鍵command + u
可以一次執(zhí)行所有的測(cè)試药蜻,也可以點(diǎn)擊每個(gè)測(cè)試方法旁的播放按鈕執(zhí)行單獨(dú)的測(cè)試。
實(shí)踐
那么替饿,我們?cè)鯓觼韺懸粋€(gè)測(cè)試用例呢语泽?測(cè)試用例的意義在于,驗(yàn)證某個(gè)類的某個(gè)行為在某種上下文中是否能得到預(yù)期的結(jié)果视卢。通常踱卵,我們可以根據(jù) Given-When-Then 模式來組織我們的測(cè)試用例,將測(cè)試用例拆分成三個(gè)部分据过。
- Given:準(zhǔn)備測(cè)試功能的上下文惋砂,包括測(cè)試方法需要的參數(shù)等。
- When:執(zhí)行真正要測(cè)試的代碼绳锅。
- Then:根據(jù)功能執(zhí)行的結(jié)果斷言測(cè)試是否通過西饵。
例:
- (void)testThatItDoesURLEncoding {
// given
NSString *searchQuery = @"$content$amp;?@"; HTTPRequest *request = [HTTPRequest requestWithURL:@"/search?q=%@", searchQuery];
// when
NSString *encodedURL = request.URL;
// then
XCTAssertEqualObjects(encodedURL, @"/search?q=%24%26%3F%40");
}
在 Then 階段,XCTest
框架提供了多個(gè)斷言宏供我們使用:
//生成一個(gè)失敗的測(cè)試
XCTFail(format…)
//為空判斷鳞芙,a1為空時(shí)通過眷柔,反之不通過
XCTAssertNil(a1, format...)
//不為空判斷,a1不為空時(shí)通過原朝,反之不通過
XCTAssertNotNil(a1, format…)
//當(dāng)expression求值為TRUE時(shí)通過
XCTAssert(expression, format...)
//當(dāng)expression求值為TRUE時(shí)通過
XCTAssertTrue(expression, format...)
//當(dāng)expression求值為False時(shí)通過
XCTAssertFalse(expression, format...)
//判斷相等驯嘱,[a1 isEqual:a2]值為TRUE時(shí)通過
XCTAssertEqualObjects(a1, a2, format...)
//判斷不等,[a1 isEqual:a2]值為False時(shí)通過
XCTAssertNotEqualObjects(a1, a2, format...)
//判斷相等(當(dāng)a1和a2是 C語(yǔ)言標(biāo)量喳坠、結(jié)構(gòu)體或聯(lián)合體時(shí)使用,實(shí)際測(cè)試發(fā)現(xiàn)NSString也可以)
XCTAssertEqual(a1, a2, format...)
//判斷不等(當(dāng)a1和a2是 C語(yǔ)言標(biāo)量鞠评、結(jié)構(gòu)體或聯(lián)合體時(shí)使用)
XCTAssertNotEqual(a1, a2, format...)
//判斷相等,(double或float類型)提供一個(gè)誤差范圍壕鹉,當(dāng)在誤差范圍(+/-accuracy)以內(nèi)相等時(shí)通過測(cè)試
XCTAssertEqualWithAccuracy(a1, a2, accuracy, format...)
//判斷不等剃幌,(double或float類型)提供一個(gè)誤差范圍,當(dāng)在誤差范圍以內(nèi)不等時(shí)通過測(cè)試
XCTAssertNotEqualWithAccuracy(a1, a2, accuracy, format...)
//異常測(cè)試晾浴,當(dāng)expression發(fā)生異常時(shí)通過
XCTAssertThrows(expression, format...)
//異常測(cè)試锥忿,當(dāng)expression發(fā)生specificException異常時(shí)通過;反之發(fā)生其他異车±撸或不發(fā)生異常均不通過
XCTAssertThrowsSpecific(expression, specificException, format...)
//異常測(cè)試敬鬓,當(dāng)expression發(fā)生具體異常、具體異常名稱的異常時(shí)通過測(cè)試,反之不通過
XCTAssertThrowsSpecificNamed(expression, specificException, exception_name, format...)
XCTAssertNoThrow(expression, format…) //異常測(cè)試钉答,當(dāng)expression沒有發(fā)生異常時(shí)通過測(cè)試础芍;
//異常測(cè)試,當(dāng)expression沒有發(fā)生具體異常数尿、具體異常名稱的異常時(shí)通過測(cè)試仑性,反之不通過
XCTAssertNoThrowSpecific(expression, specificException, format...)
//異常測(cè)試,當(dāng)expression沒有發(fā)生具體異常右蹦、具體異常名稱的異常時(shí)通過測(cè)試诊杆,反之不通過
XCTAssertNoThrowSpecificNamed(expression, specificException, exception_name, format...)
另外,如果多個(gè)測(cè)試用例類需要一些相同的初始化條件何陆,我們可以實(shí)現(xiàn)一個(gè)XCTestCase
類的派生類作為基類晨汹。在這個(gè)類中實(shí)現(xiàn)一些公共的方法和屬性。之后贷盲,測(cè)試用例類直接繼承自這個(gè)基類淘这,要注意在[setUp]
方法和[tearDown]
方法中調(diào)用 super 的實(shí)現(xiàn)。
網(wǎng)絡(luò)請(qǐng)求的測(cè)試
由于單元測(cè)試是在主線程中進(jìn)行的巩剖,因此如果只是在網(wǎng)絡(luò)請(qǐng)求異步響應(yīng)的方法中執(zhí)行斷言铝穷,那么測(cè)試在異步操作返回結(jié)果前就已經(jīng)結(jié)束了,無法達(dá)到測(cè)試的目的佳魔。對(duì)于異步操作的測(cè)試曙聂,XCTest
框架提供了這樣一種機(jī)制,首先在測(cè)試方法中關(guān)聯(lián)一個(gè)代表期望的XCTestExpectation
實(shí)例鞠鲜,然后在測(cè)試方法結(jié)束前執(zhí)行方法[- waitForExpectationsWithTimeout:handler:]
筹陵,該方法會(huì)執(zhí)行一個(gè) run loop 直到所有的期望實(shí)例執(zhí)行了方法[- fulfill]
(即期望達(dá)成)或者達(dá)到超時(shí)時(shí)間。例如 AFNetworking 中的一個(gè)測(cè)試:
- (void)testDataTaskDoesReportDownloadProgress {
NSURLSessionDataTask *task;
__weak XCTestExpectation *expectation = [self expectationWithDescription:@"Progress should equal 1.0"];
task = [self.localManager
dataTaskWithRequest:[self bigImageURLRequest]
uploadProgress:nil
downloadProgress:^(NSProgress * _Nonnull downloadProgress) {
if (downloadProgress.fractionCompleted == 1.0) {
[expectation fulfill];
}
}
completionHandler:nil];
[task resume];
[self waitForExpectationsWithCommonTimeoutUsingHandler:nil];
}
通過上述機(jī)制镊尺,雖然我們可以直接測(cè)試真正的網(wǎng)絡(luò)請(qǐng)求,但是真實(shí)網(wǎng)絡(luò)環(huán)境是非常復(fù)雜的并思,返回的響應(yīng)具有不確定性庐氮,為了達(dá)到單元測(cè)試關(guān)注點(diǎn)單一的目的,我們可能需要模擬確定的網(wǎng)絡(luò)請(qǐng)求響應(yīng)數(shù)據(jù)宋彼。OHHTTPStubs 通過NSURLProtocol
實(shí)現(xiàn)了模擬網(wǎng)絡(luò)請(qǐng)求響應(yīng)的功能弄砍,是在對(duì)網(wǎng)絡(luò)請(qǐng)求相關(guān)代碼進(jìn)行單元測(cè)試時(shí),非常好用的工具输涕。這篇文章對(duì)其實(shí)現(xiàn)原理進(jìn)行了介紹:《如何進(jìn)行 HTTP Mock(iOS)》
如果是通過 Cocoapods 來安裝管理 OHHTTPStubs 的話音婶,那么默認(rèn)在 test target 中 import 頭是找不到 pods 中的類庫(kù)的,需要在 test target 的 Build Settings 中設(shè)置 Header Search Paths莱坎,可以復(fù)制產(chǎn)品 target 中對(duì)應(yīng)的值衣式,而其中的 pods 路徑別名也需要在 test target 中設(shè)置。