iOS單元測試

1.介紹

在講XCTest之前我們先來了解一下單元測試。單元測試(unit testing),是指對軟件中的最小可測試單元進行檢查和驗證,通過開發(fā)者編寫代碼去驗證被測代碼是否正確的一種手段,例如編寫一個測試函數(shù)去測試某一功能函數(shù)是否能正確執(zhí)行達到預(yù)期效果。在實際項目開發(fā)中使用單元測試可以提高軟件的質(zhì)量,也可以盡量早的發(fā)現(xiàn)代碼中存在的問題加以修正。

2. 簡單使用

XCTest是Xcode自帶的單元測試框架,我們可以使用該框架做功能性代碼的白盒單元測試,以自測并增強代碼健壯性。

2.1 項目中添加XCTest
2.1.1 創(chuàng)建項目時勾選該選項
  • 創(chuàng)建項目時勾選 Include Unit Tests選項

    image.png

  • 創(chuàng)建項目成功后,項目目錄下即可看到對應(yīng)的單元測試文件夾(先忽略SimpleProjectUITests UI測試)


    image.png
2.1.2 項目創(chuàng)建后添加
  • 點擊Show the Test navigator選項可以看到現(xiàn)在我們項目中是未添加單元測試的:


    image.png
  • 點擊下方?按鈕,選中New Unit Test Target選項,然后配置參數(shù):
    截屏2020-08-06 下午5.14.26.png

    點擊finish即可。
2.2 方法簡單介紹

現(xiàn)在只有一個.m文件,里面有4個方法:

// 在每一個測試方法調(diào)用前,都會被調(diào)用
// 用來初始化 test 用例的一些初始值
- (void)setUp {
    // Put setup code here. This method is called before the invocation of each test method in the class.
}

// 在每一個測試方法調(diào)用后,都會被調(diào)用
// 用來重置 test 方法的數(shù)值
- (void)tearDown {
    // Put teardown code here. This method is called after the invocation of each test method in the class.
}

- (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.
    }];
}

在編寫測試代碼時,需要知道以下幾點:

  • setUp方法
    setUp方法會在XCTestCase的測試方法每次調(diào)用之前調(diào)用,所以可以把一些測試代碼需要用的初始化代碼和全局變量寫在這個方法里;

  • tearDown
    在每個單元測試方法執(zhí)行完畢后,XCTest會執(zhí)行tearDown方法,所以可以把需要測試完成后銷毀的內(nèi)容寫在這個里,以便保證下面的測試不受本次測試影響

  • 測試用例
    所有測試的方法都需要以test為前綴進行命名,比如- (void)testExample

  • 為業(yè)務(wù)類創(chuàng)建測試類
    對于每一個業(yè)務(wù)類,我們都會有一個對應(yīng)的測試類,所有的測試類需要繼承XCTestCase,比如:NetService對應(yīng)NetServiceTest,如果類的內(nèi)容太多,可以通過Category進行分類,如果某個方法暫時不想測試了,可以加一個Disable前綴。

2.3 簡單使用
    1. 我們在項目里面創(chuàng)建一個Student類:
// Student.h 文件
@interface Student : NSObject

- (NSInteger)studyAddA:(NSInteger)a b:(NSInteger)b;

- (NSInteger)studyDeleteA:(NSInteger)a b:(NSInteger)b;

@end

// Student.m 文件
#import "Student.h"

@implementation Student

- (NSInteger)studyAddA:(NSInteger)a b:(NSInteger)b{
    NSInteger result = a + b;
    return result;
}

- (NSInteger)studyDeleteA:(NSInteger)a b:(NSInteger)b{
    NSInteger result = a - b;
    return result;
}

@end

    1. 然后創(chuàng)建Student對應(yīng)的測試類:StudentTests:
#import "Student.h"

@interface StudentTests : XCTestCase

@property (nonatomic, strong) Student *student;

@end

@implementation StudentTests

- (void)setUp {
    self.student = [Student new];
}

- (void)tearDown {
    self.student = nil;
}

- (void)testStudentAdd {
    NSInteger result = [self.student studyAddA:2 b:3];
    XCTAssert(result == 5, @"結(jié)果計算出錯");
}

@end
    1. 運行測試用例

代碼編輯器邊欄菱形按鈕,測試單個用例
Test 導(dǎo)航欄,測試單個用例
快捷鍵? + U測試全部用例
使用命令行工具 xcodebuild 可以測試單個用例,也可以測試全部用例。

image.png
    1. 觀察測試結(jié)果
image.png
    1. 查看代碼覆蓋率
      打開Edit Scheme:
      image.png

勾選Gather coverage for:

image.png

然后重新,運行測試用例,觀察結(jié)果:


image.png

3. 如何進行性能測試

性能測試通過度量代碼塊執(zhí)行所消耗的時間長短,來衡量是否通過測試。
性能測試會運行想要評估的代碼塊十次,收集平均執(zhí)行時間和運行的標(biāo)準(zhǔn)偏差。然后平均值與baseLine進行比較以評估成功或失敗。

baseLine是我們指定的用來評估測試通過或者失敗的值。我們也可以自己指定一個特定的值。

截屏2020-08-07 下午4.45.20.png

我們可以通過點擊measureBlock:方法左邊菱形圓心 icon ,來設(shè)置Baseline,設(shè)置之后需要點擊save保存。之后再執(zhí)行測試用例時,如果成功,左邊的icon會從圓心變成一個 ?。

3.1 如何進行性能測試

相關(guān) API :

  • measureBlock:
- (void)testPerformanceOfMyFunction {

    [self measureBlock:^{
        // Do that thing you want to measure.
        MyFunction();
    }];
}
  • measureMetrics:automaticallyStartMeasuring:forBlock:
- (void)testMyFunction2_WallClockTime {
    [self measureMetrics:[self class].defaultPerformanceMetrics automaticallyStartMeasuring:NO forBlock:^{

        // Do setup work that needs to be done for every iteration but you don't want to measure before the call to -startMeasuring
        SetupSomething();
        [self startMeasuring];

        // Do that thing you want to measure.
        MyFunction();
        [self stopMeasuring];

        // Do teardown work that needs to be done for every iteration but you don't want to measure after the call to -stopMeasuring
        TeardownSomething();
    }];
}

4. 異步測試

什么時候需要使用異步測試:

  1. 打開文檔
  2. 在后臺線程中執(zhí)行的服務(wù)和網(wǎng)絡(luò)活動
  3. 執(zhí)行動畫
  4. UI 測試時
4.1 異步測試XCTestExpectation

異步測試分為3個部分: 新建期望 、等待期望被履行履行期望 。

  • XCTestExpectation:測試期望,可以由測試類持有,也可以自己持有,自己持有測試期望時靈活性更好一些,你可以選擇等待哪些期望。
// 測試類持有的初始化方法
XCTestExpectation *expect1 = [self expectationWithDescription:@"asyncTest1"];

// 自己持有的初始化方法
XCTestExpectation *expect2 = [[XCTestExpectation alloc] initWithDescription:@"asyncTest3"];
  • waitForExpectations:timeout: :等待異步的期望代碼執(zhí)行,根據(jù)初始化方式不同,等待的方法不同。
// 測試類持有時的等待方法
[self waitForExpectationsWithTimeout:10.0 handler:nil];

// 自己持有時的等待方法
[self waitForExpectations:@[expect3] timeout:10.0];
  • fulfill :履行期望,并且適當(dāng)加入XCTAssertTrue等斷言,來驗證測試結(jié)果。
XCTestExpectation *expect3 = [[XCTestExpectation alloc] initWithDescription:@"asyncTest3"];

[TTFakeNetworkingInstance requestWithService:apiRecordList completionHandler:^(NSDictionary *response) {
    XCTAssertTrue([response[@"code"] isEqualToString:@"200"]);
    [expect3 fulfill];
}];

[self waitForExpectations:@[expect3] timeout:10.0];
4.2 異步測試XCTWaiter

XCTWaiter是 2017 年新增的異步測試方案,可以通過代理方式來處理異常情況。

XCTWaiter *waiter = [[XCTWaiter alloc] initWithDelegate:self];
    
XCTestExpectation *expect4 = [[XCTestExpectation alloc] initWithDescription:@"asyncTest3"];
    
[TTFakeNetworkingInstance requestWithService:@"product.list" completionHandler:^(NSDictionary *response) {
    XCTAssertTrue([response[@"code"] isEqualToString:@"200"]);
    expect4 fulfill];
}];

XCTWaiterResult result = [waiter waitForExpectations:@[expect4] timeout:10 enforceOrder:NO];

XCTAssert(result == XCTWaiterResultCompleted, @"failure: %ld", result);

XCTWaiterDelegate:如果委托是XCTestCase實例,下方代理被調(diào)用時會報告為測試失敗。

// 如果有期望超時,則調(diào)用。 
- (void)waiter:(XCTWaiter *)waiter didTimeoutWithUnfulfilledExpectations:(NSArray<XCTestExpectation *> *)unfulfilledExpectations;

// 當(dāng)履行的期望被強制要求按順序履行,但期望以錯誤的順序被履行,則調(diào)用。
- (void)waiter:(XCTWaiter *)waiter fulfillmentDidViolateOrderingConstraintsForExpectation:(XCTestExpectation *)expectation requiredExpectation:(XCTestExpectation *)requiredExpectation;

// 當(dāng)某個期望被標(biāo)記為被倒置,則調(diào)用。 
- (void)waiter:(XCTWaiter *)waiter didFulfillInvertedExpectation:(XCTestExpectation *)expectation;

// 當(dāng) waiter 在 fullfill 和超時之前被打斷,則調(diào)用。 
- (void)nestedWaiter:(XCTWaiter *)waiter wasInterruptedByTimedOutWaiter:(XCTWaiter *)outerWaiter;

5. 斷言記錄

在寫測試用例的時候,我們可以使用斷言,下面是記錄一下:

XCTFail(format…) 生成一個失敗的測試; 
 
XCTAssertNil(a1, format...)為空判斷,a1為空時通過,反之不通過; 
 
XCTAssertNotNil(a1, format…)不為空判斷,a1不為空時通過,反之不通過;
 
XCTAssert(expression, format...)當(dāng)expression求值為TRUE時通過; 
 
XCTAssertTrue(expression, format...)當(dāng)expression求值為TRUE時通過; 
 
XCTAssertFalse(expression, format...)當(dāng)expression求值為False時通過; 
 
XCTAssertEqualObjects(a1, a2, format...)判斷相等,[a1 isEqual:a2]值為TRUE時通過,其中一個不為空時,不通過;
 
XCTAssertNotEqualObjects(a1, a2, format...)判斷不等,[a1 isEqual:a2]值為False時通過;
 
XCTAssertEqual(a1, a2, format...)判斷相等(當(dāng)a1和a2是 C語言標(biāo)量、結(jié)構(gòu)體或聯(lián)合體時使用,實際測試發(fā)現(xiàn)NSString也可以); 
 
XCTAssertNotEqual(a1, a2, format...)判斷不等(當(dāng)a1和a2是 C語言標(biāo)量、結(jié)構(gòu)體或聯(lián)合體時使用);
 
XCTAssertEqualWithAccuracy(a1, a2, accuracy, format...)判斷相等,(double或float類型)提供一個誤差范圍,當(dāng)在誤差范圍(+/-accuracy)以內(nèi)相等時通過測試; 
 
XCTAssertNotEqualWithAccuracy(a1, a2, accuracy, format...) 判斷不等,(double或float類型)提供一個誤差范圍,當(dāng)在誤差范圍以內(nèi)不等時通過測試; 
 
XCTAssertThrows(expression, format...)異常測試,當(dāng)expression發(fā)生異常時通過;反之不通過;(很變態(tài)) XCTAssertThrowsSpecific(expression, specificException, format...) 異常測試,當(dāng)expression發(fā)生specificException異常時通過;反之發(fā)生其他異?;虿话l(fā)生異常均不通過; 
 
XCTAssertThrowsSpecificNamed(expression, specificException, exception_name, format...)異常測試,當(dāng)expression發(fā)生具體異常、具體異常名稱的異常時通過測試,反之不通過; 
 
XCTAssertNoThrow(expression, format…)異常測試,當(dāng)expression沒有發(fā)生異常時通過測試;
 
XCTAssertNoThrowSpecific(expression, specificException, format...)異常測試,當(dāng)expression沒有發(fā)生具體異常、具體異常名稱的異常時通過測試,反之不通過; 
 
XCTAssertNoThrowSpecificNamed(expression, specificException, exception_name, format...)異常測試,當(dāng)expression沒有發(fā)生具體異常、具體異常名稱的異常時通過測試,反之不通過
 
 
 
特別注意下XCTAssertEqualObjects和XCTAssertEqual。
 
XCTAssertEqualObjects(a1, a2, format...)的判斷條件是[a1 isEqual:a2]是否返回一個YES。
 
XCTAssertEqual(a1, a2, format...)的判斷條件是a1 == a2是否返回一個YES。
 
對于后者,如果a1和a2都是基本數(shù)據(jù)類型變量,那么只有a1 == a2才會返回YES。例如

合理使用測試基類和測試工具類,可以避免大量重復(fù)測試代碼。時間轉(zhuǎn)換工具類是一個沒有外部依賴的類,當(dāng)一些對外部有依賴的類需要測試時,可以嘗試 OCMock ,它能幫助你模擬數(shù)據(jù)。另外,當(dāng)你覺得測試框架提供的斷言方法無法滿足你時,也可以試著使用 OCHamcrest

6. 未完待續(xù)

簡單的記錄一下,還有很多等待發(fā)現(xiàn)。。。

7. 參考

iOS開發(fā)之XCTest
官方文檔翻譯
在XCode中使用XCTest
iOS 單元測試和 UI 測試快速入門
官方文檔
XCTest 測試實戰(zhàn)
OCMock翻譯

最后編輯于
?著作權(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ù)。

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