一、單元測(cè)試簡(jiǎn)介
1.1、簡(jiǎn)介
單元測(cè)試(Unit Testing),又稱(chēng)為模塊測(cè)試,是指對(duì)軟件中的最小可測(cè)試單元進(jìn)行檢查和驗(yàn)證,通過(guò)開(kāi)發(fā)者編寫(xiě)代碼去驗(yàn)證被測(cè)代碼是否正確的一種手段,例如編寫(xiě)一個(gè)測(cè)試函數(shù)去測(cè)試某一功能函數(shù)是否能正確執(zhí)行達(dá)到預(yù)期效果。在實(shí)際項(xiàng)目開(kāi)發(fā)中使用單元測(cè)試可以提高軟件的質(zhì)量,也可以盡量早的發(fā)現(xiàn)代碼中存在的問(wèn)題加以修正。
執(zhí)行單元測(cè)試,是為了證明某段代碼的行為確實(shí)和開(kāi)發(fā)者所期望的一致。因此,我們所要測(cè)試的是規(guī)模很小的、非常獨(dú)立的功能片段。通過(guò)對(duì)所有單獨(dú)部分的行為建立起信心。然后,才能開(kāi)始測(cè)試整個(gè)系統(tǒng)。
1.2、單測(cè)應(yīng)用
持續(xù)集成(Continuous Integration),簡(jiǎn)稱(chēng)CI,是軟件開(kāi)發(fā)周期的一種實(shí)踐,把代碼倉(cāng)庫(kù)(Gitlab或者Github)、構(gòu)建工具(如Jenkins)和測(cè)試工具(SonarQube)集成在一起,頻繁的將代碼合并到主干然后自動(dòng)進(jìn)行構(gòu)建和測(cè)試。簡(jiǎn)單來(lái)說(shuō)持續(xù)集成就是一個(gè)監(jiān)控版本控制系統(tǒng)中代碼變化的工具,當(dāng)發(fā)生變化是可以自動(dòng)編譯和測(cè)試以及執(zhí)行后續(xù)自定義動(dòng)作。

二、單元測(cè)試使用
點(diǎn)擊下載Demo:ZJHUnitTestDemo
2.1、創(chuàng)建測(cè)試項(xiàng)目
在創(chuàng)建項(xiàng)目時(shí)勾選 Include Tests選項(xiàng),如下圖所示:

創(chuàng)建項(xiàng)目成功后,項(xiàng)目目錄下即可看到對(duì)應(yīng)的單元測(cè)試文件夾。先忽略ZJHUnitTestDemoUITests,它屬于UI測(cè)試,其他文章會(huì)有更多介紹,本文主要講ZJHUnitTestDemoTests文件

2.1.2、項(xiàng)目創(chuàng)建后添加
如果之前的項(xiàng)目還沒(méi)有添加單元測(cè)試target,也可以按照下圖方式進(jìn)行新建:

2.2、單元測(cè)試類(lèi)介紹
在新建的測(cè)試文件代碼如下所示,系統(tǒng)自動(dòng)生成了幾個(gè)方法:
#import <XCTest/XCTest.h>
// 所有的測(cè)試類(lèi)需要繼承 XCTestCase
@interface ZJHUnitTestDemoTests : XCTestCase
@end
@implementation ZJHUnitTestDemoTests
/// 在每一個(gè)測(cè)試方法調(diào)用前,都會(huì)被調(diào)用;用來(lái)初始化 test 用例的一些初始值
- (void)setUp {
// Put setup code here. This method is called before the invocation of each test method in the class.
// 在這里設(shè)置代碼。在調(diào)用類(lèi)中的每個(gè)測(cè)試方法之前調(diào)用此方法。
}
/// 在每一個(gè)測(cè)試方法調(diào)用后,都會(huì)被調(diào)用;用來(lái)重置 test 方法的數(shù)值
- (void)tearDown {
// Put teardown code here. This method is called after the invocation of each test method in the class.
// 在這里輸入刪除代碼。在調(diào)用類(lèi)中的每個(gè)測(cè)試方法之后調(diào)用此方法。
}
/// 測(cè)試方法命名以 test 開(kāi)始
- (void)testExample {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
// 這是一個(gè)功能測(cè)試用例。
// 使用XCTAssert和相關(guān)函數(shù)來(lái)驗(yàn)證您的測(cè)試產(chǎn)生正確的結(jié)果。
}
/// 性能測(cè)試
- (void)testPerformanceExample {
// This is an example of a performance test case.
// 這是一個(gè)性能測(cè)試用例。
[self measureBlock:^{
// Put the code you want to measure the time of here.
// 把你想要測(cè)量時(shí)間的代碼放在這里。
}];
}
@end
setUp方法:setUp方法會(huì)在XCTestCase的測(cè)試方法每次調(diào)用之前調(diào)用,所以可以把一些測(cè)試代碼需要用的初始化代碼和全局變量寫(xiě)在這個(gè)方法里;
tearDown:在每個(gè)單元測(cè)試方法執(zhí)行完畢后,XCTest會(huì)執(zhí)行tearDown方法,所以可以把需要測(cè)試完成后銷(xiāo)毀的內(nèi)容寫(xiě)在這個(gè)里,以便保證下面的測(cè)試不受本次測(cè)試影響
測(cè)試用例:所有測(cè)試的方法都需要以test為前綴進(jìn)行命名,比如- (void)testExample,- (void)testPerformanceExample
2.3、新建示例
我們?cè)陧?xiàng)目里面創(chuàng)建一個(gè)ZJHMathTool類(lèi):
@interface ZJHMathTool : NSObject
- (int)sumA:(int)a andB:(int)b;
- (int)subA:(int)a andB:(int)b;
- (int)multiplyA:(int)a andB:(int)b;
- (int)divideA:(int)a andB:(int)b;
@end
@implementation ZJHMathTool
- (int)sumA:(int)a andB:(int)b {
return a + b;
}
- (int)subA:(int)a andB:(int)b {
return a - b;
}
- (int)multiplyA:(int)a andB:(int)b {
return a * b;
}
- (int)divideA:(int)a andB:(int)b {
return a / b;
}
@end
同時(shí)在新建一個(gè)對(duì)應(yīng)的 ZJHMathToolTests測(cè)試類(lèi):
#import <XCTest/XCTest.h>
@interface ZJHMathToolTests : XCTestCase
@end
@implementation ZJHMathToolTests
- (void)setUp {
}
- (void)tearDown {
}
- (void)testExample {
}
- (void)testPerformanceExample {
[self measureBlock:^{
}];
}
@end
2.4、邏輯測(cè)試
接下來(lái)我們開(kāi)始編寫(xiě)用例,來(lái)測(cè)試ZJHMathTool中的方法,如下所示
@interface ZJHMathToolTests : XCTestCase
@property (nonatomic, strong) ZJHMathTool *mathTool;
@end
@implementation ZJHMathToolTests
// 新建ZJHMathTool對(duì)象
- (void)setUp {
self.mathTool = [ZJHMathTool new];
}
// 銷(xiāo)毀ZJHMathTool對(duì)象
- (void)tearDown {
self.mathTool = nil;
}
// 測(cè)試加法
- (void)testMathAdd {
int result = [self.mathTool sumA:2 andB:3];
XCTAssert(result == 5, @"加法計(jì)算出錯(cuò)");
}
// 測(cè)試減法
- (void)testMathSub {
int result = [self.mathTool subA:5 andB:2];
XCTAssert(result == 3, @"減法計(jì)算出錯(cuò)");
}
@end
運(yùn)行測(cè)試用例 :
代碼編輯器邊欄菱形按鈕,測(cè)試單個(gè)用例
Test 導(dǎo)航欄,測(cè)試單個(gè)用例
快捷鍵 command + U測(cè)試全部用例
使用命令行工具 xcodebuild 可以測(cè)試單個(gè)用例,也可以測(cè)試全部用例

2.5、性能測(cè)試
性能測(cè)試通過(guò)度量代碼塊執(zhí)行所消耗的時(shí)間長(zhǎng)短,來(lái)衡量是否通過(guò)測(cè)試。
2.5.1、測(cè)試方法準(zhǔn)備
新建 ZJHPerson 類(lèi),然后添加一個(gè)循環(huán)打印方法。
@interface ZJHPerson : NSObject
- (void)sayHello;
@end
@implementation ZJHPerson
- (void)sayHello {
for (int i = 0; i < 1000; i++) {
NSLog(@"hello");
}
}
@end
然后再新建 ZJHPerson 對(duì)象測(cè)試類(lèi) ZJHPersonTests。
#import <XCTest/XCTest.h>
#import "ZJHPerson.h"
@interface ZJHPersonTests : XCTestCase
@property (nonatomic, strong) ZJHPerson *person;
@end
@implementation ZJHPersonTests
- (void)setUp {
self.person = [ZJHPerson new];
}
- (void)tearDown {
self.person = nil;
}
- (void)testPerformanceExample {
[self measureBlock:^{
[self.person sayHello];
}];
}
@end
2.5.2、性能測(cè)試API
有兩個(gè)API可以使用
- measureBlock主要是通過(guò)block內(nèi)部代碼塊的執(zhí)行時(shí)間來(lái)測(cè)試性能,通過(guò)設(shè)置baseline(基準(zhǔn))和stddev(標(biāo)準(zhǔn)偏差)來(lái)判斷方法是否能通過(guò)性能測(cè)試。
- (void)testPerformanceOfMyFunction {
[self measureBlock:^{
//做你想測(cè)量的東西。
MyFunction();
}];
}
- measureMetrics:automaticallyStartMeasuring:forBlock測(cè)量代碼塊的性能,可以選擇推遲測(cè)量的起點(diǎn)。
- startMeasuring在代碼塊中開(kāi)始性能度量。
- stopMeasuring結(jié)束代碼塊內(nèi)的性能度量。
defaultPerformanceMetrics標(biāo)識(shí)在調(diào)用measureBlock:時(shí)度量的性能指標(biāo)。
XCTPerformanceMetricXCTest可以測(cè)量的性能指標(biāo)。
- (void)testMyFunction2_WallClockTime {
[self measureMetrics:[self class].defaultPerformanceMetrics automaticallyStartMeasuring:NO forBlock:^{
// 做設(shè)置工作,需要為每個(gè)迭代,但你不希望在調(diào)用- startmeasurement之前進(jìn)行測(cè)量
SetupSomething();
[self startMeasuring];
// 做你想測(cè)量的東西。
MyFunction();
[self stopMeasuring];
//執(zhí)行每次迭代都需要執(zhí)行的分解工作,但你不想在調(diào)用- stopmeasurement后進(jìn)行度量
TeardownSomething();
}];
}
2.5.3、設(shè)置基準(zhǔn)線(xiàn)
所有的性能測(cè)試需要設(shè)置一個(gè)Baseline來(lái)驗(yàn)證是否通過(guò)測(cè)試,沒(méi)有設(shè)置的會(huì)提示No baseline average for Time。點(diǎn)擊左邊灰色菱形圖標(biāo)可查看性能測(cè)試結(jié)果。

在性能測(cè)試結(jié)果圖里可以看到平均時(shí)間(總時(shí)長(zhǎng)/10),還有10個(gè)柱狀圖,這個(gè)意思是在這個(gè)測(cè)試方法運(yùn)行總時(shí)長(zhǎng)被分為10份,藍(lán)色柱子表示每份的耗時(shí),中間的橫線(xiàn)表示平均時(shí)間,點(diǎn)擊數(shù)字可查看每份中的平均時(shí)長(zhǎng)。
- Metric:度量單位,Time為時(shí)間
- Result:度量結(jié)果
- Average:度量時(shí)間平均值
- Baseline:度量的基準(zhǔn)線(xiàn)
- Max STDDEV:最大容錯(cuò)率
點(diǎn)擊Edit可以進(jìn)行編輯,我設(shè)置的基準(zhǔn)時(shí)間是0.15s,最大容錯(cuò)率是10%,運(yùn)行結(jié)果是0.147s,好于基本先2%,所有可以通過(guò)。設(shè)置時(shí)基準(zhǔn)時(shí)間為0.05s時(shí)就會(huì)出錯(cuò)。

2.6、異步測(cè)試
什么時(shí)候需要使用異步測(cè)試:
- 打開(kāi)文檔
- 在后臺(tái)線(xiàn)程中執(zhí)行的服務(wù)和網(wǎng)絡(luò)活動(dòng)
- 執(zhí)行動(dòng)畫(huà)
- UI 測(cè)試時(shí)
2.6.1 異步測(cè)試XCTestExpectation
異步測(cè)試分為3個(gè)部分: 新建期望 、等待期望被履行 和 履行期望 。
XCTestExpectation:測(cè)試期望,可以由測(cè)試類(lèi)持有,也可以自己持有,自己持有測(cè)試期望時(shí)靈活性更好一些,你可以選擇等待哪些期望。
waitForExpectations:timeout: :等待異步的期望代碼執(zhí)行,根據(jù)初始化方式不同,等待的方法不同。
fulfill :履行期望,并且適當(dāng)加入XCTAssertTrue等斷言,來(lái)驗(yàn)證測(cè)試結(jié)果。
/// 異步測(cè)試XCTestExpectation:測(cè)試類(lèi)持有期望
- (void)testAsyncMethod1 {
// 新建期望:測(cè)試類(lèi)持有的初始化方法
XCTestExpectation *expect1 = [self expectationWithDescription:@"asyncTest1"];
// 履行期望:執(zhí)行異步操作
[ZJHNetworkTool requestUrl:@"getTestData" param:@{} completion:^(NSDictionary * _Nonnull respondDic) {
// 異步結(jié)束,標(biāo)注期望達(dá)成
[expect1 fulfill];
}];
// 等待期望被履行:測(cè)試類(lèi)持有時(shí)的等待方法
[self waitForExpectationsWithTimeout:3.0 handler:^(NSError * _Nullable error) {
NSLog(@"***ZJH error : %@", error);
}];
}
/// 異步測(cè)試XCTestExpectation:自己類(lèi)持有期望
- (void)testAsyncMethod2 {
// 新建期望:自己持有的初始化方法
XCTestExpectation *expect2 = [[XCTestExpectation alloc] initWithDescription:@"asyncTest2"];
// 履行期望:執(zhí)行異步操作
[ZJHNetworkTool requestUrl:@"getTestData" param:@{} completion:^(NSDictionary * _Nonnull respondDic) {
XCTAssertTrue([respondDic[@"code"] isEqualToString:@"200"]);
// 異步結(jié)束,標(biāo)注期望達(dá)成
[expect2 fulfill];
}];
// 等待期望被履行:自己持有時(shí)的等待方法
[self waitForExpectations:@[expect2] timeout:3];
}
2.6.2 異步測(cè)試XCTWaiter
XCTWaiter是 2017 年新增的異步測(cè)試方案,可以通過(guò)代理方式來(lái)處理異常情況。
XCTWaiterDelegate:如果委托是XCTestCase實(shí)例,下方代理被調(diào)用時(shí)會(huì)報(bào)告為測(cè)試失敗。
/// 異步測(cè)試XCTWaiter
- (void)testAsyncMethod3 {
// 新建期望
XCTWaiter *waiter = [[XCTWaiter alloc] initWithDelegate:self];
XCTestExpectation *expect3 = [[XCTestExpectation alloc] initWithDescription:@"asyncTest3"];
// 履行期望:執(zhí)行異步操作
[ZJHNetworkTool requestUrl:@"getTestData" param:@{} completion:^(NSDictionary * respondDic) {
XCTAssertTrue([respondDic[@"code"] isEqualToString:@"200"]);
// 異步結(jié)束,標(biāo)注期望達(dá)成
[expect3 fulfill];
}];
// 等待期望被履行
XCTWaiterResult result = [waiter waitForExpectations:@[expect3]
timeout:3
enforceOrder:NO];
XCTAssert(result == XCTWaiterResultCompleted, @"failure: %ld", result);
}
// 如果有期望超時(shí),則調(diào)用。
- (void)waiter:(XCTWaiter *)waiter didTimeoutWithUnfulfilledExpectations:(NSArray<XCTestExpectation *> *)unfulfilledExpectations {
NSLog(@"***ZJH 如果有期望超時(shí),則調(diào)用。");
}
// 當(dāng)履行的期望被強(qiáng)制要求按順序履行,但期望以錯(cuò)誤的順序被履行,則調(diào)用。
- (void)waiter:(XCTWaiter *)waiter fulfillmentDidViolateOrderingConstraintsForExpectation:(XCTestExpectation *)expectation requiredExpectation:(XCTestExpectation *)requiredExpectation {
NSLog(@"***ZJH 當(dāng)履行的期望被強(qiáng)制要求按順序履行,但期望以錯(cuò)誤的順序被履行,則調(diào)用。");
}
// 當(dāng)某個(gè)期望被標(biāo)記為被倒置,則調(diào)用。
- (void)waiter:(XCTWaiter *)waiter didFulfillInvertedExpectation:(XCTestExpectation *)expectation {
NSLog(@"***ZJH 當(dāng)某個(gè)期望被標(biāo)記為被倒置,則調(diào)用。");
}
// 當(dāng) waiter 在 fullfill 和超時(shí)之前被打斷,則調(diào)用。
- (void)nestedWaiter:(XCTWaiter *)waiter wasInterruptedByTimedOutWaiter:(XCTWaiter *)outerWaiter {
NSLog(@"***ZJH 當(dāng) waiter 在 fullfill 和超時(shí)之前被打斷,則調(diào)用。");
}
三、其他補(bǔ)充
3.1、斷言記錄
在寫(xiě)測(cè)試用例的時(shí)候,我們可以使用斷言,下面是記錄一下:
XCTFail(format…) 生成一個(gè)失敗的測(cè)試;
XCTAssertNil(a1, format...)為空判斷,a1為空時(shí)通過(guò),反之不通過(guò);
XCTAssertNotNil(a1, format…)不為空判斷,a1不為空時(shí)通過(guò),反之不通過(guò);
XCTAssert(expression, format...)當(dāng)expression求值為T(mén)RUE時(shí)通過(guò);
XCTAssertTrue(expression, format...)當(dāng)expression求值為T(mén)RUE時(shí)通過(guò);
XCTAssertFalse(expression, format...)當(dāng)expression求值為False時(shí)通過(guò);
XCTAssertEqualObjects(a1, a2, format...)判斷相等,[a1 isEqual:a2]值為T(mén)RUE時(shí)通過(guò),其中一個(gè)不為空時(shí),不通過(guò);
XCTAssertNotEqualObjects(a1, a2, format...)判斷不等,[a1 isEqual:a2]值為False時(shí)通過(guò);
XCTAssertEqual(a1, a2, format...)判斷相等(當(dāng)a1和a2是 C語(yǔ)言標(biāo)量、結(jié)構(gòu)體或聯(lián)合體時(shí)使用,實(shí)際測(cè)試發(fā)現(xiàn)NSString也可以);
XCTAssertNotEqual(a1, a2, format...)判斷不等(當(dāng)a1和a2是 C語(yǔ)言標(biāo)量、結(jié)構(gòu)體或聯(lián)合體時(shí)使用);
XCTAssertEqualWithAccuracy(a1, a2, accuracy, format...)判斷相等,(double或float類(lèi)型)提供一個(gè)誤差范圍,當(dāng)在誤差范圍(+/-accuracy)以?xún)?nèi)相等時(shí)通過(guò)測(cè)試;
XCTAssertNotEqualWithAccuracy(a1, a2, accuracy, format...) 判斷不等,(double或float類(lèi)型)提供一個(gè)誤差范圍,當(dāng)在誤差范圍以?xún)?nèi)不等時(shí)通過(guò)測(cè)試;
XCTAssertThrows(expression, format...)異常測(cè)試,當(dāng)expression發(fā)生異常時(shí)通過(guò);反之不通過(guò);(很變態(tài)) XCTAssertThrowsSpecific(expression, specificException, format...) 異常測(cè)試,當(dāng)expression發(fā)生specificException異常時(shí)通過(guò);反之發(fā)生其他異?;虿话l(fā)生異常均不通過(guò);
XCTAssertThrowsSpecificNamed(expression, specificException, exception_name, format...)異常測(cè)試,當(dāng)expression發(fā)生具體異常、具體異常名稱(chēng)的異常時(shí)通過(guò)測(cè)試,反之不通過(guò);
XCTAssertNoThrow(expression, format…)異常測(cè)試,當(dāng)expression沒(méi)有發(fā)生異常時(shí)通過(guò)測(cè)試;
XCTAssertNoThrowSpecific(expression, specificException, format...)異常測(cè)試,當(dāng)expression沒(méi)有發(fā)生具體異常、具體異常名稱(chēng)的異常時(shí)通過(guò)測(cè)試,反之不通過(guò);
XCTAssertNoThrowSpecificNamed(expression, specificException, exception_name, format...)異常測(cè)試,當(dāng)expression沒(méi)有發(fā)生具體異常、具體異常名稱(chēng)的異常時(shí)通過(guò)測(cè)試,反之不通過(guò)
特別注意下XCTAssertEqualObjects和XCTAssertEqual。
XCTAssertEqualObjects(a1, a2, format...)的判斷條件是[a1 isEqual:a2]是否返回一個(gè)YES。
XCTAssertEqual(a1, a2, format...)的判斷條件是a1 == a2是否返回一個(gè)YES。
對(duì)于后者,如果a1和a2都是基本數(shù)據(jù)類(lèi)型變量,那么只有a1 == a2才會(huì)返回YES。例如
3.2、查看代碼覆蓋率
3.2.1、Edit Scheme 勾選配置
Command+shit+, 調(diào)出工程配置 Test->Options->Code Coverage勾選上

3.2.2、結(jié)果查看
運(yùn)行測(cè)試后,command+9或 者點(diǎn)擊工程左上角最后一個(gè)圖標(biāo)查看覆蓋報(bào)告

3.2.3、代碼查看
雙擊方法名或者點(diǎn)擊方法名右側(cè)的箭頭可以跳轉(zhuǎn)到該方法中。右側(cè)有數(shù)字,0表示沒(méi)有覆蓋掉,1表示覆蓋了一次,調(diào)用了幾次數(shù)字就會(huì)變成幾。

3.3、跳過(guò)部分測(cè)試
在 Xcode 10 中新增功能,在 Edit Scheme -> Test -> Info -> Tests 中可以通過(guò)取消勾選,來(lái)選擇跳過(guò)部分測(cè)試用例。在 target 的 Options 選項(xiàng)中,Automatically includes new tests,選項(xiàng)是默認(rèn)勾選的,新建的測(cè)試文件會(huì)自動(dòng)添加進(jìn)去。

3.4、測(cè)試用例的執(zhí)行順序
默認(rèn)情況下,測(cè)試用例執(zhí)行的順序是按字母順序來(lái)執(zhí)行的,按固定順序執(zhí)行可能會(huì)使一些隱式的依賴(lài)關(guān)系無(wú)法被發(fā)現(xiàn)?,F(xiàn)在有了隨機(jī)的執(zhí)行順序,就可以挖掘出那些隱式的依賴(lài)關(guān)系??梢栽?Edit Scheme -> Test -> Info -> Tests -> Options 中開(kāi)啟該功能。

3.5、并行測(cè)試
并行測(cè)試可以同時(shí)進(jìn)行多個(gè)測(cè)試,從而節(jié)省大量時(shí)間。在測(cè)試時(shí)會(huì)啟動(dòng)多個(gè)模擬器,模擬器之間的數(shù)據(jù)都是隔離的,可以在 Edit Scheme -> Test -> Info -> Tests -> Options 中開(kāi)啟該功能。如上圖
對(duì)于并行測(cè)試的一些建議:
- 某個(gè)測(cè)試用例需要消耗大量時(shí)間的類(lèi),可以拆分成多個(gè)類(lèi)并行測(cè)試,從而節(jié)省時(shí)間。
- 你需要清楚哪些測(cè)試在并行執(zhí)行時(shí)是不安全的,避免并行執(zhí)行這些測(cè)試。
- 性能測(cè)試的可以統(tǒng)一放在一個(gè) Bundle 中,禁用并行執(zhí)行。
參考鏈接:
iOS單元測(cè)試:http://m.itdecent.cn/p/fcd82723f134
iOS開(kāi)發(fā)之單元測(cè)試:http://m.itdecent.cn/p/6f4f9fe5f1e1
iOS 單元測(cè)試和 UI 測(cè)試快速入門(mén):https://juejin.cn/post/6844903744170098695
iOS單元測(cè)試:https://juejin.cn/post/6844904138388537352