iOS開發(fā)之單元測試

教學(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)

單元測試好處

對比圖.png
  • 好處:
    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選項

新建項目.png

新建一對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文件新建單元測試類文件

新建步驟.png

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

生成文件.png

新建的PersonTests文件代碼如下圖所示,系統(tǒng)自動生成了幾個方法

  • setUp
    每個類中 測試方法調(diào)用前 先調(diào)用這個方法 以方便 開發(fā)者 做些 測試前的準備
  • tearDown
    當這個 類中的 所有的 測試 方法 測試完后 會調(diào)用這個方法
PersonTests文件代碼.png

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è)的菱形按鈕開始單獨測試這一個方法。

image.png

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

測試通過.png

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

測試未通過.png

控制臺也會打印錯誤信息

錯誤信息.png

下面是一些常用的斷言


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文件代碼如下:

性能測試代碼.png

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

第一次運行測試.png

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

性能測試結(jié)果圖.png

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

測試結(jié)果圖.png

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

測試未通過.png

異步測試

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測試文件

image.png
image.png

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

文件挪動.png

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

錄制過程.gif

錄制中自動生成的代碼如下所示,但是發(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];這樣運行測試代碼就會自動運行項目了。

測試過程.gif

查看測試覆蓋率

Command+shit+, 調(diào)出工程配置 Test->Options->Code Coverage勾選上

工程配置.png

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

覆蓋日志.png

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容