簡介
單元測試(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 測試初窺