教學(xué)視頻:
https://www.bilibili.com/video/BV1y4411X7Qt?p=1
參考博客:
https://blog.csdn.net/cwhzm/article/details/72598803
http://www.itdecent.cn/p/c54f0cc08c20
什么是單元測試?
單元測試是開發(fā)者編寫的一小段代碼,用于檢驗被測代碼中的一個很明確的功能是否正確。通常而言,一個單元測試是用于判斷某個特定條件(或者場景)下某個特定函數(shù)的行為。
執(zhí)行單元測試,是為了證明某段代碼的行為確實和開發(fā)者所期望的一致。因此,我們所要測試的是規(guī)模很小的、非常獨立的功能片段。通過對所有單獨部分的行為建立起信心。然后,才能開始測試整個系統(tǒng)
單元測試好處

-
好處:
1、單元測試使工作完成的更輕松
2、經(jīng)過單元測試的代碼,質(zhì)量能夠得到保證
3、單元測試發(fā)現(xiàn)的問題很容易定位。
4、修改代碼犯的錯,經(jīng)過單元測試易發(fā)現(xiàn)
5、單元測試可以在早期就發(fā)現(xiàn)性能問題
6、單元測試使你的設(shè)計更好
7、大大減少花在調(diào)試上的時間
創(chuàng)建項目
新建項目時要勾選Include Tests選項

新建一對Person類文件
Person.h文件代碼
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) NSInteger age;
- (instancetype)initWithInfo:(NSDictionary *)info;
@end
NS_ASSUME_NONNULL_END
Person.m文件內(nèi)代碼
#import "Person.h"
@implementation Person
- (instancetype)initWithInfo:(NSDictionary *)info {
self = [super init];
if (self) {
self.name = info[@"name"];
self.age = [info[@"age"] integerValue];
}
return self;
}
@end
邏輯測試
為Person文件新建單元測試類文件

取名一般是需要測試的類名+Tests,這里我新建了一個PersonTests文件。

新建的PersonTests文件代碼如下圖所示,系統(tǒng)自動生成了幾個方法
-
setUp
每個類中 測試方法調(diào)用前 先調(diào)用這個方法 以方便 開發(fā)者 做些 測試前的準備 -
tearDown
當這個 類中的 所有的 測試 方法 測試完后 會調(diào)用這個方法

PersonTests文件內(nèi)新增一個測試方法,用來測試Person類的- (instancetype)initWithInfo:(NSDictionary *)info方法
先引入Person類#import "Person.h"
- (void)testInitPerson {
NSDictionary *dic = @{@"name" : @"Jonas", @"age" : @25};
Person *p = [[Person alloc] initWithInfo:dic];
NSAssert([p.name isEqualToString:dic[@"name"]], @"姓名不一致");
NSAssert(p.age == [dic[@"age"] integerValue], @"年齡不一致");
}
點擊測試方法左側(cè)的菱形按鈕開始單獨測試這一個方法。

或者按組合鍵Command+U運行所有測試文件,經(jīng)過一段時間后,看到方法左邊有個綠色的勾則表示通過測試,有一個紅色的叉表示未通過。

下面我故意將年齡寫錯,制造錯誤

控制臺也會打印錯誤信息

下面是一些常用的斷言
XCTFail(@"this is a fail test"); // 生成一個失敗的測試
XCTAssertNil(@"not a nil string",@"string must be nil"); // XCTAssertNil(a1, format...) 為空判斷, a1 為空時通過,反之不通過;
XCTAssertNil(@"",@"string must be nil"); // 注意@"" 一樣無法通過
XCTAssertNil(nil,@"object must be nil");
XCTAssertNotNil(a1, format…) 不為nil 判斷,a1不為 nil 時通過,反之不通過;
// 注意空 和 nil 還是有區(qū)別的
XCTAssertNotNil(@"not nil string", @"string can not be nil");
XCTAssert(expression, format...) // 當expression求值為TRUE時通過; expression 為一個表達式
XCTAssert((2 > 2), @"expression must be true");
XCTAssert((3>2),@"expression is true");
XCTAssertTrue(expression, format...) // 當expression求值為TRUE時通過;>0 的都視為 true
XCTAssertTrue(1, @"Can not be zero");
XCTAssertFalse(expression, format...) 當expression求值為False時通過;
XCTAssertFalse((2 < 2), @"expression must be false");
XCTAssertEqualObjects(a1, a2, format...) // 判斷相等, [a1 isEqual:a2] 值為TRUE時通過,其中一個不為空時,不通過;
XCTAssertEqualObjects(@"1", @"1", @"[a1 isEqual:a2] should return YES");
XCTAssertEqualObjects(@"1", @"2", @"[a1 isEqual:a2] should return YES");
XCTAssertNotEqualObjects(a1, a2, format...) 判斷不等, [a1 isEqual:a2] 值為False時通過,
XCTAssertNotEqualObjects(@"1", @"1", @"[a1 isEqual:a2] should return NO");
XCTAssertNotEqualObjects(@"1", @"2", @"[a1 isEqual:a2] should return NO");
XCTAssertEqual(a1, a2, format...) // 判斷相等(當a1和a2是 C語言標量、結(jié)構(gòu)體或聯(lián)合體時使用,實際測試發(fā)現(xiàn)NSString也可以);
// 1.比較基本數(shù)據(jù)類型變量
XCTAssertEqual(1, 2, @"a1 = a2 shoud be true"); // 無法通過測試
XCTAssertEqual(1, 1, @"a1 = a2 shoud be true"); // 通過測試
// 2.比較NSString對象
NSString *str1 = @"1";
NSString *str2 = @"1";
// NSString *str3 = str1;
XCTAssertEqual(str1, str2, @"a1 and a2 should point to the same object"); // 通過測試
XCTAssertEqual(str1, str3, @"a1 and a2 should point to the same object"); // 通過測試
// 3.比較NSArray對象
NSArray *array1 = @[@1];
NSArray *array2 = @[@1];
NSArray *array3 = array1;
XCTAssertEqual(array1, array2, @"a1 and a2 should point to the same object"); // 無法通過測試
XCTAssertEqual(array1, array3, @"a1 and a2 should point to the same object"); // 通過測試
XCTAssertNotEqual(a1, a2, format...) // 判斷不等(當a1和a2是 C語言標量、結(jié)構(gòu)體或聯(lián)合體時使用);
XCTAssertEqualWithAccuracy(a1, a2, accuracy, format...) // 判斷相等,(double或float類型)提供一個誤差范圍,當在誤差范圍(+/- accuracy )以內(nèi)相等時通過測試;
XCTAssertEqualWithAccuracy(1.0f, 1.5f, 0.25f, @"a1 = a2 in accuracy should return NO"); // 測試沒法通過
XCTAssertNotEqualWithAccuracy(a1, a2, accuracy, format...) // 判斷不等,(double或float類型)提供一個誤差范圍,當在誤差范圍以內(nèi)不等時通過測試;
XCTAssertNotEqualWithAccuracy(1.0f, 1.5f, 0.25f, @"a1 = a2 in accuracy should return NO"); // 測試通過
XCTAssertThrows(expression, format...) // 異常測試,當expression發(fā)生異常時通過;反之不通過;(很變態(tài))
XCTAssertThrowsSpecific(expression, specificException, format...) // 異常測試,當expression發(fā)生 specificException 異常時通過;反之發(fā)生其他異?;虿话l(fā)生異常均不通過;
XCTAssertThrowsSpecificNamed(expression, specificException, exception_name, format...) // 異常測試,當expression發(fā)生具體異常、具體異常名稱的異常時通過測試,反之不通過;
XCTAssertNoThrow(expression, format…) // 異常測試,當expression沒有發(fā)生異常時通過測試;
XCTAssertNoThrowSpecific(expression, specificException, format...) // 異常測試,當expression沒有發(fā)生具體異常、具體異常名稱的異常時通過測試,反之不通過;
XCTAssertNoThrowSpecificNamed(expression, specificException, exception_name, format...) // 異常測試,當expression沒有發(fā)生具體異常、具體異常名稱的異常時通過測試,反之不通過
性能測試
先為Person類添加一個循環(huán)打印方法。
- (void)sayHello {
for (int i = 0; i < 1000; i++) {
NSLog(@"hello");
}
}
PersonTests.m文件代碼如下:

第一次運行測試代碼后如下所示,點擊左邊灰色菱形圖標可查看性能測試結(jié)果

在性能測試結(jié)果圖里可以看到平均時間(總時長/10),還有10個柱狀圖,這個意思是在這個測試方法運行總時長被分為10份,藍色柱子表示每份的耗時,中間的橫線表示平均時間,點擊數(shù)字可查看每份中的平均時長。

點擊Set Baseline可以為該性能測試增加基準線,再點擊Edit按鈕可設(shè)置基準線和最大容錯率?,F(xiàn)在我設(shè)置的基準線是0.1 s最大容錯率是10%。所以如果平均時間超過0.11 s就報錯。

可以看到重新運行后,超過了130%,所以會報錯。

異步測試
Person類定義一個異步耗時方法
+ (void)asyncMethodWithCompletion:(void (^)(void))completion {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 10000; i++) {
NSLog(@"hello");
}
dispatch_async(dispatch_get_main_queue(), ^{
if(completion) {
completion();
}
});
});
}
測試文件定義一個測試異步方法
- (void)testAsyncMethod {
// 定義一個預(yù)期
XCTestExpectation *exp = [self expectationWithDescription:@"Person異步方法的期望"];
[Person asyncMethodWithCompletion:^{
// 異步結(jié)束,標注期望達成
[exp fulfill];
}];
// 等待期望異步執(zhí)行完,若在2秒內(nèi)完成,則通過,否則不通過
[self waitForExpectationsWithTimeout:2 handler:^(NSError * _Nullable error) {
// 期望回調(diào),根據(jù)error是否為空可知是否通過
}];
}
運行測試方法,在期望時間內(nèi)通過則出現(xiàn)綠勾,否則出現(xiàn)紅叉。
UI測試
新建UI測試文件


將UI測試文件從Tests文件夾挪到UITests文件夾

首先Command+R運行項目,運行完成后將光標置于UI測試方法內(nèi),然后點擊左下角紅色錄制按鈕,項目又會運行一次,在界面上做一系列操作會在UI測試方法內(nèi)自動生成代碼,操作完成后點擊左下角停止錄制按鈕。

錄制中自動生成的代碼如下所示,但是發(fā)現(xiàn)會有點問題,每次按鍵點擊都寫了2次,所以我們手動刪掉一次。
注意:這里有一個坑,如果直接運行這段代碼,會報錯,因為第一個A鍵點擊后模擬器的鍵盤上A按鍵會變成小寫的a按鍵,此時點擊第二次A鍵時就會報錯鍵盤上找不到這個A按鍵。
- (void)testViewControllerInputUI {
XCUIApplication *app = [[XCUIApplication alloc] init];
[[[[[[app.windows childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeTextField] elementBoundByIndex:0] tap];
XCUIElement *aKey = app.keys[@"A"];
[aKey tap];
[aKey tap];
XCUIElement *sKey = app.keys[@"s"];
[sKey tap];
[sKey tap];
XCUIElement *dKey = app.keys[@"d"];
[dKey tap];
[dKey tap];
XCUIElement *fKey = app.keys[@"f"];
[fKey tap];
[fKey tap];
XCUIElement *element = [[[[[app childrenMatchingType:XCUIElementTypeWindow] elementBoundByIndex:0] childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element;
[[[element childrenMatchingType:XCUIElementTypeTextField] elementBoundByIndex:1] tap];
XCUIElement *zKey = app.keys[@"Z"];
[zKey tap];
[zKey tap];
XCUIElement *xKey = app.keys[@"x"];
[xKey tap];
[xKey tap];
XCUIElement *cKey = app.keys[@"c"];
[cKey tap];
[cKey tap];
[[element childrenMatchingType:XCUIElementTypeButton].element tap];
}
修改后代碼
- (void)testViewControllerInputUI {
XCUIApplication *app = [[XCUIApplication alloc] init];
[[[[[[app.windows childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeTextField] elementBoundByIndex:0] tap];
XCUIElement *aKey = app.keys[@"A"];
[aKey tap];
XCUIElement *sKey = app.keys[@"s"];
[sKey tap];
XCUIElement *dKey = app.keys[@"d"];
[dKey tap];
XCUIElement *fKey = app.keys[@"f"];
[fKey tap];
XCUIElement *element = [[[[[app childrenMatchingType:XCUIElementTypeWindow] elementBoundByIndex:0] childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element;
[[[element childrenMatchingType:XCUIElementTypeTextField] elementBoundByIndex:1] tap];
XCUIElement *zKey = app.keys[@"Z"];
[zKey tap];
XCUIElement *xKey = app.keys[@"x"];
[xKey tap];
XCUIElement *cKey = app.keys[@"c"];
[cKey tap];
[[element childrenMatchingType:XCUIElementTypeButton].element tap];
}
當然我們也可以自己寫UI測試的代碼,想執(zhí)行什么操作就寫什么代碼。下面的代碼就是我自己寫了段點擊tf1輸入abcdefg然后點擊tf2輸入12345,然后點擊2次刪除鍵后點擊按鈕??梢园l(fā)現(xiàn)自己寫的代碼邏輯要更清晰一些,代碼也要更美觀。
- (void)testViewControllerInputUI {
XCUIApplication *app = [[XCUIApplication alloc] init];
XCUIElement *tf1 = [[[[[app.windows childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeTextField] elementBoundByIndex:0];
[tf1 tap];
[tf1 typeText:@"abcdefg"];
XCUIElement *tf2 = [[[[[app.windows childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeTextField] elementBoundByIndex:1];
[tf2 tap];
[tf2 typeText:@"12345"];
XCUIElement *deleteKey = app.keys[@"delete"];
[deleteKey tap];
[deleteKey tap];
XCUIElement *button = [[[[app.windows childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeButton].element;
[button tap];
}
點擊測試方法左邊的灰色菱形按鈕,開始測試UI,可以看到手機會自動的執(zhí)行代碼描述的行為。運行測試方法前需要先運行項目,不想每次都自己運行,可以在- (void)setUp方法里加上運行app代碼[[[XCUIApplication alloc] init] launch];這樣運行測試代碼就會自動運行項目了。

查看測試覆蓋率
Command+shit+, 調(diào)出工程配置 Test->Options->Code Coverage勾選上

運行測試后,command+9或者點擊工程左上角最后一個圖標查看覆蓋報告

雙擊方法名或者點擊方法名右側(cè)的箭頭可以跳轉(zhuǎn)到該方法中。