iOS單元測試-XCTest

goddess.JPG

前言

單元測試簡單來說,就是為了方便測試一些功能是否正常運(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í)單元測試之前,讓我們先來看看一些常用第三方所選用的測試框架:

圖1.jpg

從圖中得知,蘋果官方的測試框架XCTest 還是很受歡迎的哈 ~

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

圖2.png
圖3.png

一、集成

  1. 創(chuàng)建工程的時(shí)候,直接勾選 Include Unit Tests
屏幕快照_2018-08-01_下午6_33_49.png

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

圖4.png

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

圖5.png

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

圖6.png

二、方法

測試用例類 .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í)別。

三、使用

  1. 快捷鍵 Command + U 會(huì)運(yùn)行全部單元測試;
  2. 鼠標(biāo)放在方法右邊,會(huì)出現(xiàn)播放按鈕,點(diǎn)擊后開始單個(gè)方法的測試;
圖7.png
  1. 鼠標(biāo)放在方法左邊,會(huì)出現(xiàn)播放按鈕,點(diǎn)擊后開始單個(gè)方法的測試;


    圖8.png
  2. 如測試通過,會(huì)有“Test Succeeded”提示,且函數(shù)左邊菱形圖標(biāo)展示為綠色;如測試不通過,會(huì)有“Test Failed”提示,且函數(shù)左邊菱形圖標(biāo)展示為紅色。

圖9.png

四、測試

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ù),則測試不通過,如下:

圖10.png
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才能找到頭文件

  1. 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

圖11.png

也可以從控制臺(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單元測試

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

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

  • 單元測試在維基百科上的解釋 在計(jì)算機(jī)編程中,單元測試(英語:Unit Testing)又稱為模塊測試, 是針對(duì)程序...
    愚蟲閱讀 1,004評(píng)論 0 0
  • 原文地址:https://www.raywenderlich.com/150073/ios-unit-testin...
    默默熊閱讀 5,120評(píng)論 1 67
  • 簡介 測試目的:模擬多種可能性,減少錯(cuò)誤,增強(qiáng)健壯性,提高穩(wěn)定性。 測試種類:在iOS中的通常分為單元測試和UI測...
    i順頌時(shí)宜閱讀 9,375評(píng)論 0 39
  • 用到的組件 1、通過CocoaPods安裝 2、第三方類庫安裝 3、第三方服務(wù) 友盟社會(huì)化分享組件 友盟用戶反饋 ...
    SunnyLeong閱讀 15,191評(píng)論 1 180
  • 再過2個(gè)小時(shí),我終于可以完成上半年所有考試了!從3月中旬開始準(zhǔn)備職稱考試考試,我也不知道我今年的考試品種會(huì)如此之多...
    XD_0de4閱讀 241評(píng)論 0 0

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