前言
單元測試簡單來說,就是為了方便測試一些功能是否正常運(yùn)行,調(diào)試接口是否能正常使用,用代碼去檢測代碼是否正確的一種手段。例如:你為了測試某一個(gè)網(wǎng)絡(luò)接口,每次都重新啟動(dòng),經(jīng)過很多操作之后,才測試到那個(gè)網(wǎng)絡(luò)接口。如果使用了單元測試,就可以直接測試那個(gè)方法,相對(duì)方便很多。單元測試不僅沒有降低我們Coding的效率,也能保證在之后的改動(dòng)中及時(shí)發(fā)現(xiàn)可能出現(xiàn)的錯(cuò)誤。
學(xué)習(xí)單元測試之前,讓我們先來看看一些常用第三方所選用的測試框架:

從圖中得知,蘋果官方的測試框架XCTest 還是很受歡迎的哈 ~
并不是所有的方法都需要測試,一般而言,私有方法不需要測試,只有暴露在 .h 中的方法需要測試。那到底測試用例的覆蓋率是多少才合適吶?其實(shí)一個(gè)軟件覆蓋度在50%以上就可以稱為一個(gè)健壯的軟件了,要達(dá)到70,80這些已經(jīng)是非常難了,但“史萊克從來不缺少天才”,例如:AFNNetWorking的覆蓋率高達(dá)88%,SDWebImage的覆蓋率也達(dá)到75%。


一、集成
- 創(chuàng)建工程的時(shí)候,直接勾選 Include Unit Tests

2.如果已有項(xiàng)目未勾選,則通過以下方式再創(chuàng)建一個(gè)
File-->new-->Target-->iOS-->iOS Unit Testing Bundle

3.工程創(chuàng)建好之后,找到系統(tǒng)單元測試Tests 文件夾,在 .m文件中就可以寫我們的測試用例了,是不是很簡單吶~

4.一般我們會(huì)新建不同的測試用例類與代碼類一一對(duì)應(yīng),可以通過新建 Unit Test Case Class 來實(shí)現(xiàn)

二、方法
測試用例類 .m 文件中,會(huì)有幾個(gè)默認(rèn)方法,我們來看下這幾個(gè)方法是什么時(shí)候調(diào)用和他們的作用:
- (void)setUp {
[super setUp];
//初始化,在測試方法調(diào)用之前調(diào)用
}
- (void)tearDown {
// 釋放測試用例的資源代碼,這個(gè)方法會(huì)每個(gè)測試用例執(zhí)行后調(diào)用
[super tearDown];
}
- (void)testExample {
// 測試用例的例子,注意測試用例一定要test開頭
}
- (void)testPerformanceExample {
[self measureBlock:^{
// 需要測試性能的代碼
}];
}
注意:測試用例必須以Test開頭,且不能有參數(shù),不然不會(huì)被識(shí)別。
三、使用
- 快捷鍵 Command + U 會(huì)運(yùn)行全部單元測試;
- 鼠標(biāo)放在方法右邊,會(huì)出現(xiàn)播放按鈕,點(diǎn)擊后開始單個(gè)方法的測試;

-
鼠標(biāo)放在方法左邊,會(huì)出現(xiàn)播放按鈕,點(diǎn)擊后開始單個(gè)方法的測試;
圖8.png 如測試通過,會(huì)有“Test Succeeded”提示,且函數(shù)左邊菱形圖標(biāo)展示為綠色;如測試不通過,會(huì)有“Test Failed”提示,且函數(shù)左邊菱形圖標(biāo)展示為紅色。

四、測試
1. 基本斷言的邏輯測試,關(guān)于斷言會(huì)在文末說明;
例1:有一個(gè)函數(shù)目的是生成在[base, end]之間的隨機(jī)數(shù),我們來檢測一下會(huì)不會(huì)出現(xiàn)越界的情況:
// 生成在[base, end]之間的隨機(jī)數(shù)
- (int)randomNumberFrom: (int)base End: (int)top{
if (base >= top) {
return base;
}
return (arc4random() % (top - base + 1)) + base;
}
- (void)testRandom{
int base = 3;
int top = 80;
for (int i=0; i<100; i++) {
int temp = [self randomNumberFrom:base End:top];
if (temp < base || temp > top) {
XCTFail(@"invalid num = %d",temp);
}
}
}
例2:在ViewController中寫一個(gè)簡單的方法
- (int)getNum{
return 100;
}
在測試的文件中導(dǎo)入ViewController.h,并且定義一個(gè)vc屬性
#import <XCTest/XCTest.h>
#import "ViewController.h"
@interface MJViewControllerTest : XCTestCase
@property (nonatomic, strong) ViewController *VC;
@end
@implementation MJViewControllerTest
測試用例的實(shí)現(xiàn)
- (void)setUp {
[super setUp];
self.VC = [[ViewController alloc]init];
}
- (void)tearDown {
self.VC = nil;
[super tearDown];
}
- (void)testGetNum{
int result = [self.VC getNum];
XCTAssertEqual(result, 100, @"不相等,測試不通過");
}
運(yùn)行測試用例,可以看到測試通過,菱形圖標(biāo)顯示綠色。
如果這時(shí)我們改下斷言,把100隨便改成一個(gè)數(shù),則測試不通過,如下:

2. 異步測試
代碼中會(huì)有很多異步的場景需要驗(yàn)證,例如網(wǎng)絡(luò)請(qǐng)求callback中執(zhí)行的操作,由于測試方法主線程執(zhí)行完就會(huì)結(jié)束,所以需要在方法結(jié)束前設(shè)置等待,調(diào)回回來的時(shí)候再讓它繼續(xù)執(zhí)行,如果超時(shí)或者是遇到斷言的失敗,該用例會(huì)失敗。
注意:使用pod的項(xiàng)目中,在XC測試框架中測試內(nèi)容包括第三方包時(shí),需要手動(dòng)去設(shè)置Header Search Paths才能找到頭文件
-
expectationForNotification方法 ,該方法監(jiān)聽一個(gè)通知,如果在規(guī)定時(shí)間內(nèi)正確收到通知?jiǎng)t測試通過。
#define WAIT do {\
[self expectationForNotification:@"MJUnitTest" object:nil handler:nil];\
[self waitForExpectationsWithTimeout:30 handler:nil];\
} while (0);
// waitForExpectationsWithTimeout是等待時(shí)間,超過了就不再等待往下執(zhí)行。
#define NOTIFY \
[[NSNotificationCenter defaultCenter]postNotificationName:@"MJUnitTest" object:nil];
- (void)testRequest{
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
manager.responseSerializer.acceptableContentTypes = [NSSet setWithObject:@"text/html"];
NSString *urlStr = @"http://www.weather.com.cn/data/cityinfo/101190401.html";
[manager GET:urlStr parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
NSLog(@"responseObject:%@",responseObject);
XCTAssertNotNil(responseObject, @"返回出錯(cuò)");
NOTIFY //繼續(xù)執(zhí)行
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
NSLog(@"error:%@",error);
XCTAssertNil(error, @"請(qǐng)求出錯(cuò)");
NOTIFY //繼續(xù)執(zhí)行
}];
WAIT //暫停
}
2.expectationWithDescription 來進(jìn)行異步是否完成期望的測試。
- (void)testRequest2{
XCTestExpectation *exp = [self expectationWithDescription:@"接口請(qǐng)求失敗。。。"];
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
manager.responseSerializer.acceptableContentTypes = [NSSet setWithObject:@"text/html"];
NSString *urlStr = @"http://www.weather.com.cn/data/cityinfo/101190401.html";
[manager GET:urlStr parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
NSLog(@"responseObject2:%@",responseObject);
XCTAssertNotNil(responseObject, @"返回出錯(cuò)");
//如果斷言沒問題,就調(diào)用fulfill宣布測試滿足
[exp fulfill];
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
NSLog(@"error:%@",error);
XCTAssertNil(error, @"請(qǐng)求出錯(cuò)");
[exp fulfill];
}];
//設(shè)置延遲多少秒后,如果沒有滿足測試條件就報(bào)錯(cuò)
[self waitForExpectationsWithTimeout:15 handler:^(NSError * _Nullable error) {
if (error) {
NSLog(@"Timeout Error: %@", error);
}
}];
}
3.expectationForPredicate測試方法,代碼來自于AFNetworking,用于測試backgroundImageForState方法
- (void)testThatBackgroundImageChanges {
XCTAssertNil([self.button backgroundImageForState:UIControlStateNormal]);
NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(UIButton * _Nonnull button, NSDictionary<NSString *,id> * _Nullable bindings) {
return [button backgroundImageForState:UIControlStateNormal] != nil;
}];
[self expectationForPredicate:predicate
evaluatedWithObject:self.button
handler:nil];
[self waitForExpectationsWithTimeout:20 handler:nil];
}
利用謂詞計(jì)算,button是否正確的獲得了backgroundImage,如果正確20秒內(nèi)正確獲得則通過測試,否則失敗。
3.性能測試
將要測量執(zhí)行時(shí)間的代碼放到testPerformanceExample方法內(nèi)部的block中:
- (void)testPerformanceExample {
[self measureBlock:^{
NSMutableArray * mutArray = [[NSMutableArray alloc] init];
for (int i = 0; i < 10000; i++) {
NSObject * object = [[NSObject alloc] init];
[mutArray addObject:object];
}
}];
}
在block中寫一個(gè)for循環(huán)執(zhí)行10000次,然后點(diǎn)擊方法左邊的菱形圖標(biāo),得到:average: 0.003sec

也可以從控制臺(tái)打印信息獲取程序運(yùn)行10次的時(shí)間,取一個(gè)平均運(yùn)行時(shí)間值:
measured [Time, seconds] average: 0.003, relative standard deviation: 9.329%,
values: [0.002840, 0.002487, 0.003074, 0.002515, 0.002386, 0.002313, 0.002351, 0.002362, 0.002455, 0.002741],
五、斷言
XCTFail(format…) 生成一個(gè)失敗的測試;
XCTAssertNil(a1, format...)為空判斷,a1為空時(shí)通過,反之不通過;
XCTAssertNotNil(a1, format…)不為空判斷,a1不為空時(shí)通過,反之不通過;
XCTAssert(expression, format...)當(dāng)expression求值為TRUE時(shí)通過;
XCTAssertTrue(expression, format...)當(dāng)expression求值為TRUE時(shí)通過;
XCTAssertFalse(expression, format...)當(dāng)expression求值為False時(shí)通過;
XCTAssertEqualObjects(a1, a2, format...)判斷相等,[a1 isEqual:a2]值為TRUE時(shí)通過,其中一個(gè)不為空時(shí),不通過;
XCTAssertNotEqualObjects(a1, a2, format...)判斷不等,[a1 isEqual:a2]值為False時(shí)通過;
XCTAssertEqual(a1, a2, format...)判斷相等(當(dāng)a1和a2是 C語言標(biāo)量、結(jié)構(gòu)體或聯(lián)合體時(shí)使用, 判斷的是變量的地址,如果地址相同則返回TRUE,否則返回NO);
XCTAssertNotEqual(a1, a2, format...)判斷不等(當(dāng)a1和a2是 C語言標(biāo)量、結(jié)構(gòu)體或聯(lián)合體時(shí)使用);
XCTAssertEqualWithAccuracy(a1, a2, accuracy, format...)判斷相等,(double或float類型)提供一個(gè)誤差范圍,當(dāng)在誤差范圍(+/-accuracy)以內(nèi)相等時(shí)通過測試;
XCTAssertNotEqualWithAccuracy(a1, a2, accuracy, format...) 判斷不等,(double或float類型)提供一個(gè)誤差范圍,當(dāng)在誤差范圍以內(nèi)不等時(shí)通過測試;
XCTAssertThrows(expression, format...)異常測試,當(dāng)expression發(fā)生異常時(shí)通過;反之不通過;(很變態(tài)) XCTAssertThrowsSpecific(expression, specificException, format...) 異常測試,當(dāng)expression發(fā)生specificException異常時(shí)通過;反之發(fā)生其他異?;虿话l(fā)生異常均不通過;
XCTAssertThrowsSpecificNamed(expression, specificException, exception_name, format...)異常測試,當(dāng)expression發(fā)生具體異常、具體異常名稱的異常時(shí)通過測試,反之不通過;
XCTAssertNoThrow(expression, format…)異常測試,當(dāng)expression沒有發(fā)生異常時(shí)通過測試;
XCTAssertNoThrowSpecific(expression, specificException, format...)異常測試,當(dāng)expression沒有發(fā)生具體異常、具體異常名稱的異常時(shí)通過測試,反之不通過;
XCTAssertNoThrowSpecificNamed(expression, specificException, exception_name, format...)異常測試,當(dāng)expression沒有發(fā)生具體異常、具體異常名稱的異常時(shí)通過測試,反之不通過
文章附件:Demo
附錄:本來寫好了,去開了個(gè)需求會(huì)議,回來打開頁面,內(nèi)容丟了一半,(╥╯^╰╥),歷史版本也沒保存,不得不重新寫了一遍,下次一定要備份,看在辛苦的份上,喜歡的點(diǎn)個(gè)贊吧~
參考文章:
iOS單元測試(作用及入門提升)
淺談iOS單元測試
iOS單元測試初探以及OCMock使用入門
iOS-使用Xcode自帶單元測試UnitTest
iOS - UnitTests 單元測試
iOS單元測試
