iOS 單元測試 - XCTest


簡介

單元測試(Unit Testing)又稱為模塊測試,是針對程序模塊軟件設計來進行正確性檢驗的測試工作。程序單元是應用的最小可測試部件。對于面向對象編程,最小單元就是方法,包括基類、抽象類、或者派生類中的方法。

單元測試通常由軟件開發(fā)人員編寫,用于確保他們所寫的代碼符合軟件需求和遵循開發(fā)目標。通常來說,每修改一次程序就會進行最少一次單元測試,在編寫程序的過程中前后很可能要進行多次單元測試,以證實程序達到工作目標要求。

Xcode 集成了對單元測試的支持 XCTest。XCTest 是從 Xcode5 開始引入的一個測試框架,是上一代測試框架 OCUnit 的更現代化實現。XCTest 提供了與 Xcode 更好的集成。下面我們簡單介紹下XCTest的使用。

XCTest

在 Xcode 新建項目時,勾選 Unit Tests 和 UI Tests,會創(chuàng)建對應的測試 target,并創(chuàng)建了繼承于XCTestCase 的測試用例類,該類繼承自 XCTestCase 類,其中包含三個方法:setUp,tearDown和 testExample。

  • setUp 用于在測試前設置好需要用到的對象等
  • tearDown 在測試結束時調用
  • testExample 是一個測試方法,測試方法命名通常是 testXXX 的格式,且不能有參數,不然不會識別為測試方法,測試方法的執(zhí)行順序是按照方法名中 test 后面的字符順序執(zhí)行的。
  • measureBlock: 性能測試方法,將需要性能測試的代碼放入 block 里,運行這個方法會執(zhí)行多次,運行時間比對設定的標準值和偏差判斷是否可以通過測試

創(chuàng)建完成后,就可以在測試方法里,編寫測試代碼,然后點擊方法前的菱形按鈕運行測試方法, 也可以使用快捷鍵 command+u 運行整個測試單元。正確運行后顯示綠色對勾,運行錯誤會顯示紅色叉號。


斷言

大部分的測試方法使用斷言決定的測試結果。所有斷言都有一個類似的形式:比較,表達式為真假,強行失敗等。

XCTFail(format...)  直接Fail
XCTAssertNil(a1,format...)為空判斷, a1為空時通過,反之不通過;
XCTAssertNotNil(a1,format...) 不為空判斷,a1不為空時通過,反之不通過;
XCTAssert(expression,format...) 當expression求值為true時通過;
XCTAssertTrue(expression,format...) 當expression求值為true時通過;
XCTAssertFalse(expression,format...) 當expression求值為False時通過;
XCTAssertEqualObjects(a1, a2,format...) 判斷相等 [a1 isEqual:a2]值為TRUE時通過,其中一個不為空時,不通過;
XCTAssertNotEqualObjects(a1, a2,format...) 判斷不等,[a1 isEqual:a2]值為False時通過;
XCTAssertEqual(a1, a2, format...)判斷相等(當a1和a2是標量、結構體或聯(lián)合體時使用,實際測試發(fā)現NSString也可以);
XCTAssertNotEqual(a1, a2, format...)判斷不等(當a1和a2是標量、結構體或聯(lián)合體時使用);
XCTAssertEqualWithAccuracy(a1, a2, accuracy, format...)判斷相等,(double或float類型)提供一個誤差范圍,當在誤差范圍(+/-accuracy)以內相等時通過測試;
XCTAssertNotEqualWithAccuracy(a1, a2, accuracy, format...) 判斷不等,(double或float類型)提供一個誤差范圍,當在誤差范圍以內不等時通過測試;
XCTAssertThrows(expression, format...)異常測試,當expression發(fā)生異常時通過;反之不通過;(很變態(tài))
XCTAssertThrowsSpecific(expression, specificException, format...) 異常測試,當expression發(fā)生specificException異常時通過;反之發(fā)生其他異常或不發(fā)生異常均不通過;
XCTAssertThrowsSpecificNamed(expression, specificException, exception_name, format...)異常測試,當expression發(fā)生具體異常、具體異常名稱的異常時通過測試,反之不通過;
XCTAssertNoThrow(expression, format…)異常測試,當expression沒有發(fā)生異常時通過測試;
XCTAssertNoThrowSpecificNamed(expression, specificException, exception_name, format...)異常測試,當expression沒有發(fā)生具體異常、具體異常名稱的異常時通過測試,反之不通過

自定義斷言宏
在使用斷言時,經常使用一些特定情況的斷言,寫非常的啰嗦,難以閱讀。并且還都是重復代碼。可以通過編寫自己的斷言宏來解決這個問題。例如:

NSString *string = @"http";
XCTAssertTrue([string isKindOfClass:[NSString class]] && [string hasPrefix:@"http"],
    @"'%@' is not a valid URL string", string);


//自定義斷言
#define AssertIsValidURLString(a) \
if (![a isKindOfClass:[NSString class]] || ![a hasPrefix:@"http"]) { \
    XCTFail(@"'%@' is not a valid URL string", a); \
}\

NSString *text = @"123";
AssertIsValidURLString(text);

對于更復雜的斷言和檢查,可以使用簡單的輔助類,方便檢查。

異步測試

測試異步方法時,例如網絡請求等耗時操作,由于執(zhí)行結果不是立即就能獲取到,XCTest 提供了一些輔助方法,如下例所示:

- (void)testAsynExample {
    XCTestExpectation *expectation = [self expectationWithDescription:@"操作超時。。"];
    NSOperationQueue *queue = [[NSOperationQueue alloc]init];
    [queue addOperationWithBlock:^{
        sleep(2); //模擬耗時操作
        [expectation fulfill];
        XCTAssert(YES, @"fail"); //判斷異步方法的結果是否正確
    }];

    //等待 XCTestExpectation fulfill,設置延時等待多少秒,如果超時就報錯
    [self waitForExpectationsWithTimeout:1 handler:^(NSError * _Nullable error) {
        if (error) {
            NSLog(@"Error: %@", error);
        }
    }];
}

waitForExpectationsWithTimeout: 方法會在規(guī)定時間內,等待期望 XCTestExpectation 滿足 fulfill,規(guī)定時間內不滿足期望就會報錯。

異步測試除了使用 expectationWithDescription 以外,還可以使用 expectationForPredicate 和 expectationForNotification

  • expectationForPredicate
- (void)testAsynExample {
    XCTAssertNil(self.imageView.image);
    [self.imageView setImageWithURL:self.jpegURL];
    
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"image != nil"];
    [self expectationForPredicate:predicate evaluatedWithObject:self.imageView handler:nil];
    [self waitForExpectationsWithTimeout:10 handler:nil];
}

NSPredicate 謂詞判斷,是否加載出了圖片,self.imageView.image != nil,在規(guī)定時間內是否測試通過。

  • expectationForNotification 監(jiān)聽一個通知,在規(guī)定時間內等待,是否收到通知
- (void)testAsynExample {
    //....
    [self expectationForNotification:@"NotificationName" object:nil handler:nil];
    [self waitForExpectationsWithTimeout:10 handler:nil];
}

UITest

上面介紹的單元測試是對 app 的業(yè)務邏輯以及網絡接口方面的測試。下面來介紹一下 UI 的測試。 在創(chuàng)建項目時勾選 UI Tests 會創(chuàng)建對應的 UI 測試的 target,如果你要在已有項目中添加 UI Tests 的話,可以新建一個 iOS UI Testing 的 target。創(chuàng)建完成后和上面一樣也會創(chuàng)建對應的繼承于 XCTestCase 測試類。

UI 行為錄制

寫好 UI 后就可以,進行我們的 UI 測試了,在 setUp 中,我們使用 XCUIApplication 的 launch 方法來啟動測試 app。XCUIApplication 是 UIApplication 在測試進程中的代理 (proxy),我們可以在 UI 測試中通過這個類型和應用本身進行一些交互,比如開始或者終止一個 app。
然后使用 Xcode 的 UI Testing 直接錄制操作,操作如下:

點擊錄制按鈕,啟動 app,點擊 UI 就會在測試方法中,生成對應的測試代碼,看起來很厲害的樣子。

獲取 UI 元素

在錄制時,點擊輸入框,可以看到獲取 UI 元素的代碼,如下:

- (void)testExample {
    XCUIApplication *app = [[XCUIApplication alloc] init];
    XCUIElement *element = [[[[[[[[[app childrenMatchingType:XCUIElementTypeWindow] elementBoundByIndex:0] childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element;
    [[element childrenMatchingType:XCUIElementTypeTextField].element tap];
    [[element childrenMatchingType:XCUIElementTypeSecureTextField].element tap];
    
    [app.buttons[@"login"].staticTexts[@"login"] tap];
}

自動錄制生成的代碼使用了很多 query 來查詢文本框,獲取代表 app 中具體 UI 元素的 XCUIElement,然后對其進行測試操作。但是這樣產生大量代碼,難以理解,我們可使用簡潔的方法獲取 UI 元素。
在 Interface Builder 或者代碼中進行設置 textfield 的 identifier :

- (void)testExample {
    
    NSString *name = @"admin";
    NSString *pwd = @"123";
    
    XCUIApplication *app = [[XCUIApplication alloc] init];
    //獲取 name 輸入框
    XCUIElement *nameTextField = app.textFields[@"nameTextField"];
    [nameTextField tap];
    [nameTextField typeText:name]; //輸入框中寫入文字
    
    //獲取 pwd 輸入框
    XCUIElement *pwdTextField = app.secureTextFields[@"pwdTextField"];
    [pwdTextField tap];
    [pwdTextField typeText:pwd];
    
    //點擊 login 按鈕
    [app.buttons.staticTexts[@"login"] tap];
    
    //登錄需要網絡請求,等待一段時間。登錄成功 push 到下一個頁面
    //這里判斷在規(guī)定的時間內導航欄是否 push 過去
    XCUIElement *nav = app.navigationBars[name].staticTexts[name];
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"exists == 1"];
    [self expectationForPredicate:predicate evaluatedWithObject:nav handler:nil];
    [self waitForExpectationsWithTimeout:6 handler:nil];
}

上面的操作是獲取兩個輸入框,并寫入內容,點擊登錄 push 到下一個頁面。

總結

本篇文章介紹了,使用 Xcode 來進行單元測試的一些操作,可以看到還是很方便快捷的。熟練掌握單元測試的一些技巧,對于提高 app 的質量還是有很大幫助的。

References

iOS單元測試
XCTest 測試實戰(zhàn)
WWDC15 Session筆記 - Xcode 7 UI 測試初窺

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,572評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 98,071評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,409評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,569評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,360評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,895評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,979評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,123評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 48,643評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,559評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,742評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,250評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 43,981評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,363評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,622評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,354評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,707評論 2 370

推薦閱讀更多精彩內容

  • 前言 單元測試簡單來說,就是為了方便測試一些功能是否正常運行,調試接口是否能正常使用,用代碼去檢測代碼是否正確的一...
    minjing_lin閱讀 4,156評論 0 20
  • 單元測試在維基百科上的解釋 在計算機編程中,單元測試(英語:Unit Testing)又稱為模塊測試, 是針對程序...
    愚蟲閱讀 824評論 0 0
  • 前言:測試是一個好的App不可缺少的部分。每一個App都是由一個個小的功能組合到一起的。而這些小的功能又是由一個個...
    伯牙呀閱讀 6,735評論 1 19
  • 簡介 測試目的:模擬多種可能性,減少錯誤,增強健壯性,提高穩(wěn)定性。 測試種類:在iOS中的通常分為單元測試和UI測...
    i順頌時宜閱讀 9,151評論 0 39
  • 一、簡介 單元測試(Unit Testing) 是一種軟件測試方法,主要用于確定各個獨立的軟件模塊是否正確。在這個...
    陽光下的灰塵閱讀 1,260評論 0 2