一、UI測試簡介
1.1、什么是UITesting
2015 年,Apple 發布了 UI 自動化測試框架 XCUITest 并集成在 Xcode7 中,而 iOS/macOS UI 自動化測試依賴兩個核心技術:XCUITest 和 Accessibility。
XCUITest 是集成在 Xcode 中的測試框架,若想使用 UI 測試功能,可以在創建 iOS 項目時勾選 Include Tests 選項,從而使項目具備自動化測試的能力。而 Accessibility 技術,則是 Apple 官方為視障用戶提供的一整套使用 iOS/macOS App 的解決方案。
Xcode 項目創建 UITests Target 并運行測試,其編譯產物 Test App 本質上是一個 Deamon 守護進程,該進程有獨立的應用程序生命周期,依靠 XCUIApplication 類型進行管理。UITests 的 Test App 進程在運行時會驅動 Host App(項目的主 Target 產物),并且利用元素審查的相關 API 驅動 Host App 模擬用戶行為交互,從而進行 UI 自動化測試。
對于 Accessibility 技術,開發人員需要注意的是,XCUITest 框架默認并不能將所有視圖元素審查到,只會審查到可以被 VoiceOver 功能讀取文字的元素。比如,UIButton 和 UILabel,這些視圖對于視障用戶而言可以通過語音來獲知其內容,而對于 UIImageView、 UIView 這種對于視障人士并不友好的 UIKit 視圖元素默認是不會審查到的,所以編碼時要另行配置 Accessibility 相關屬性,以保證其支持 Accessibility 從而在 UI 自動化查詢的元素層級中可見。
基于 XCUITest 框架 和 Accessibility 技術的自動化測試,有利于 App 進行數據一致性校驗,但 UI 一致性校驗能力較弱。比如,App 可以針對某些數據請求結果或者某個元素是否存在進行校驗,而視覺展示效果卻仍需要人工介入。
1.2、使用UITesting
Using UI Testing:
- Complements unit testing(補充單元測試)
- Unit testing more precisely pinpoints failures(單元測試更精確地確定了失敗)
- UI testing covers broader aspects of functionality(UI測試覆蓋了函數邊界方面)
- Find the right blend of UI tests and unit tests for your project(找到好的方式融合UI測試和單元測試)
Candidates for UI Testing(使用UI測試的情況):
- Demo sequences(一些列Demo)
- Common workflows(相同的工作流程)
- Custom views(相同的視圖)
- Document creation, saving, and opening(文檔的創建、保存、打開)
1.3、UI Recording
通過 UI Recording ,可以將你操作手機的行為記錄下來,并且轉換成代碼,可以幫助你快速生成 UI 測試代碼。選中 UI 測試類,你能再下方看到一個小紅點,點擊小紅點開始錄制你的交互。
在你進行交互時,Xcode 會自動轉化成代碼,你可以借此創建新的測試代碼,也可以以此拓展已經存在的測試代碼。當然它也不是十分完美,并不是總能如你所愿,還需要你做一些處理,比如說自動生成的代碼過于繁瑣,你可以用一些更簡潔的代碼實現。即使這樣,UI Recording 也是非常高效的方式。點擊下載Demo:ZJHUnitTestDemo
二、UI 測試相關的類
2.1、XCUITest 框架結構
XCUITest 測試框架 API 主要包含:元素查詢(UI Element Queries)相關類型,如 XCUIElementQuery,UI 元素(UI Elements)相關類型,如 XCUIElement,以及測試 App 生命周期類型(Application Lifecycle)類型,如 XCUIApplication。
2.2、XCUIApplication
XCUIApplication 代表整個應用,可以用來啟動、結束進程,或者傳入一些啟動參數,最常用的功能是利用 XCUIApplication
實例來查詢 UI 上的元素。
// 返回 UI 測試 Target 設置中選中的 Target Application 的實例
- (instancetype)init;
// 根據 bundleId 返回一個應用程序實例
- (instancetype)initWithBundleIdentifier:(NSString *)bundleIdentifier;
// 啟動應用程序
- (void)launch;
// 將應用程序喚醒至前臺,在多程序聯合測試下會用到
- (void)activate;
// 結束一個正在運行的應用程序
- (void)terminate;
2.3、XCUIElement
XCUIElement 應用程序中的 UI 控件,控件類型多樣,可能是Button
,Cell
,Window
等等。該類實例有很多模擬交互的方法,如tap
模擬用戶點擊事件,swipe
模擬滑動事件,typeText:
模擬用戶輸入內容。在 UI 測試中我們需要找到某個空間,可以通過他們的類型來縮小范圍。另外還有一種方式通過 Accessibility identifer, label, title 等等方式來定位對應的控件。通過類型加 identifier 的方式來定位的控件元素的方式,可以滿足大多數場景。
也可以通過代碼的方式添加Accessibility identifer。
// 通過代碼的方式添加Accessibility identifer
for (int i = 0; i < 5; i++) {
UISwitch *swt = [UISwitch new];
CGFloat pointY = 50 * i + 100;
swt.center = CGPointMake(self.view.frame.size.width/2, pointY);
// 添加accessibility標記
swt.accessibilityLabel = [NSString stringWithFormat:@"swt-%d", I];
[self.view addSubview:swt];
}
// UI測試獲取控件
XCUIApplication *app = [[XCUIApplication alloc] init];
[app launch];
XCUIElement *codeSwt1 = app.switches[@"swt-1"];
[codeSwt1 tap];
2.4、XCUIElementQuery
XCUIElementQuery 是一個用來定位控件元素的類,一般是一組符合篩選條件的元素集合。如app.buttons
即返回 XCUIElementQuery 實例,是包含了當前所有的button
的集合,你可以再通過 XCUIElementQuery
的方法做下一步的篩選。
使用 NSPredicate 為查詢條件增加條件
// 查找所有的 collectionView 的 cell, collectionViews 和 cells 是 XCUIElementQuery 提供的方法
XCUIElementQuery *cells = app.collectionViews.cells;
// 使用 NSPredicate 為查詢條件增加條件
XCUIElementQuery *cells = [app.collectionViews.cells matchingPredicate:[NSPredicate predicateWithFormat:@"identifier LIKE '?labelPrice?'"]];
三、UI測試示例
點擊下載Demo:ZJHUnitTestDemo
3.1、使用UI Recording自動生成代碼
新建一個 UI 測試 Target,使用 UI Recording 自動生成代碼,或者也可以直接手寫。
3.2、修改UI Recording 代碼
UI Recording 的代碼識別不出中文,需要手動改下;還會點擊兩次 tag,刪除一個就好。
/// 修改 UI Recording 生成的代碼
- (void)testLogin2 {
// 拿到當前application程序
XCUIApplication *app = [[XCUIApplication alloc] init];
// 點擊 "UITestDemo" 按鈕
[app.staticTexts[@"UITestDemo"] tap];
// 點擊賬號textField
[[[[[[[[[app.windows childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeTextField] elementBoundByIndex:0] tap];
// 點擊鍵盤 shift,切換大小寫
[app.buttons[@"shift"] tap];
// 點擊鍵盤 a
XCUIElement *aKey = app.keys[@"a"];
[aKey tap];
// [aKey tap]; // 多余tag 需要注釋掉
// 點擊密碼textField
[[[[[[[[[[[app childrenMatchingType:XCUIElementTypeWindow] elementBoundByIndex:0] childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeTextField] elementBoundByIndex:1] tap];
// 切換數字鍵盤
XCUIElement *moreKey = app.keys[@"more"];
[moreKey tap];
// 輸入1、2、3、4
XCUIElement *key = app.keys[@"1"];
[key tap];
XCUIElement *key2 = app.keys[@"2"];
[key2 tap];
XCUIElement *key3 = app.keys[@"3"];
[key3 tap];
XCUIElement *key4 = app.keys[@"4"];
[key4 tap];
// 點擊登錄按鈕
XCUIElement *button = app.buttons[@"登錄"];
[button.staticTexts[@"登錄"] tap];
// 點擊鍵盤刪除按鈕
XCUIElement *deleteKey = app.keys[@"delete"];
[deleteKey tap];
// 點擊登錄按鈕
[button tap];
// 點擊返回按鈕
[app.navigationBars[@"Record List"].buttons[@"登錄"] tap];
}
3.3、精簡代碼
UI Recording 生成的代碼還不夠簡練,可以再次對其修改。也可以直接編寫,不使用UI Recording生成的
/// 精簡代碼
- (void)testLogin3 {
// 拿到當前application程序
XCUIApplication *app = [[XCUIApplication alloc] init];
// 獲取 “UITestDemo” 按鈕,并點擊,跳轉到登錄頁面
[app.staticTexts[@"UITestDemo"] tap];
// 拿到當前app下的textfeild的搜索器
XCUIElementQuery *tfQuery = app.textFields;
// 賬號textField
XCUIElement *accountTF = [tfQuery elementBoundByIndex:0];
// 密碼textField
XCUIElement *passwordTF = [tfQuery elementBoundByIndex:1];
// 拿到當前app下的button的搜索器
XCUIElementQuery *btnQuery = app.buttons;
// 獲取登錄按鈕
XCUIElement *loginBtn = btnQuery[@"登錄"];
// 模擬UI操作
[accountTF tap]; // 點擊賬號textField
[accountTF typeText:@"a"]; // 輸入字母a
[passwordTF tap];// 點擊密碼textField
[passwordTF typeText:@"1234"]; // 輸入字母123456
[loginBtn tap]; // 點擊登錄,提示密碼錯誤
// 獲取鍵盤的刪除按鈕
XCUIElement *deleteBtn = app.keys[@"delete"];
[deleteBtn tap]; // 點擊一次刪除按鈕
// 再次點擊登錄按鈕
[loginBtn tap]; // 點擊登錄,成功跳轉
// 獲取 “Record List” navigationBar,
XCUIElement *navBarElement = app.navigationBars[@"Record List"];
// 獲取返回按鈕
XCUIElement *backBtn = navBarElement.buttons[@"登錄"];
// 點擊返回按鈕
[backBtn tap];
}
注意:如果某些UI測試失敗,請禁用“連接硬件鍵盤”選項。
為此,請在模擬器應用程序中選擇“ I / O”菜單選項,然后轉到
Keyboard
并取消選中Connect hardware keyboard
。 連接硬件鍵盤后,UI測試似乎無法訪問模擬器中的text field
。
四、UI測試拓展 Tips
4.1、等待預期
可以用expectationForPredicate:evaluatedWithObject:handler:
方法監聽對象屬性,當滿足
NSPredicate條件時,
expectation相當于自動
fullfill`。如果一直不滿足條件,會一直等待直至超時,除此之外還可以用通知和 KVO 的方式實現。
例如,列表中,新增一個cell數據后,可以監聽監聽app.cells
的count
屬性,判斷cell的個數是否按預期增加,代碼如下:
// 暫存當前 cell 數量
NSInteger cellsCount = app.cells.count;
// 設置一個預期 判斷 app.cells 的 count 屬性會等于 cellsCount+1, 等待直至失敗,如果符合則不再等待
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"count == %d",cellsCount+1];
[self expectationForPredicate:predicate evaluatedWithObject:app.cells handler:nil];
// 執行添加操作,或者網絡請求等異步操作
[self addCellData];
// 等待實現預期,這里等到10s
[self waitForExpectationsWithTimeout:10 handler:nil];
4.2、多應用聯合測試
多應用聯合測試時,依賴XCUIApplication
類的以下 2 個方法:
- initWithBundleIdentifier:
- activate
前者可以根據 BundleId 獲取其他 App 的實例,讓我們可以啟動其他 App。后者可以讓 App 從后臺切換至前臺,在多應用間切換。簡單實現代碼如下:
- (void)testExample {
// 返回 UI 測試 Target 設置中選中的 Target Application 的實例
XCUIApplication *app = [[XCUIApplication alloc] init];
// 使用 BundleId 獲得另外一個 App 實例:需要先創建另個測試app
XCUIApplication *anotherApp = [[XCUIApplication alloc] initWithBundleIdentifier:@"zjh.ZJHUnitTestDemo2"];
// 先啟動我們的主 App
[app launch];
// 做一系列測試1
[app.staticTexts[@"UITestDemo"] tap];
[app.navigationBars[@"登錄"].buttons[@"Home"] tap];
sleep(2);
// 啟動另一個 App
[anotherApp activate];
sleep(2);
// 回到我們的主 App (在 App 未啟動的情況下調 activate 會讓 App 啟動)
[app activate];
// 做一系列測試2
[app.staticTexts[@"UITestDemo"] tap];
[app.navigationBars[@"登錄"].buttons[@"Home"] tap];
}
4.3、截屏
在 UI 測試中有 2 種類型支持通過代碼截屏,分別是XCUIElement
和XCUIScreen
。
// 獲取一個截屏對象
XCUIScreenshot *screenshot = [app screenshot];
// 實例化一個附件對象 并傳入截屏對象
XCTAttachment *attachment = [XCTAttachment attachmentWithScreenshot:screenshot];
// 附件的存儲策略 如果選擇 XCTAttachmentLifetimeDeleteOnSuccess 則測試成功的情況會被刪除
attachment.lifetime = XCTAttachmentLifetimeKeepAlways;
// 設置一個名字 方便區分
attachment.name = @"MyScreenshot";
[self addAttachment:attachment];
在測試結束后,可以在 Report 導航欄中查看截圖:
除此之外 Xcode 提供了自動截圖的功能,可以幫助我們在每一個交互操作之后自動截圖。此功能會產生大量截圖,需要謹慎使用,一般情況最好勾選Delete when each test succeeds
,需要在 Edit Scheme -> Test -> Options 中開啟。
4.4、被測試 app 如何判斷正在進行 UI Test
在啟動 app 時增加一個啟動參數,在 app 中讀取。
// 測試代碼
XCUIApplication *app = [[XCUIApplication alloc] init];
app.launchEnvironment = @{@"isUITest" : @YES};
[app launch];
// app 代碼
+ (BOOL)isUITesting {
NSDictionary *environment = [[NSProcessInfo processInfo] environment];
return [environment[@"isUITest"] boolValue];
}
五、Accessibility Inspector簡介
5.1、使用 Accessibility Inspector
Accessibility Inspector 輔助功能檢查器,通過輔助功能檢查器,您可以識別應用程序中無法訪問的部分。它提供了有關如何訪問它們的反饋,并模擬畫外音,以幫助您識別畫外音用戶的體驗。觀看在輔助功能檢查器中完全調試的應用程序的實時演示,并了解如何利用這個強大的工具使您的應用程序更適合每個人。
前文中提到 Apple 對于視圖元素會默認審查能夠通過 VoiceOver 播放文字的視圖元素,而對于 UIImageView、UIView 這種默認不支持 Accessibility 功能的需要配置相關特性,而開發人員在開發過程中可以通過 Accessibility Inspector 查看不同進程的 Accessibility 元素層級,該應用可以審查 iOS 和 macOS 的元素。
選擇 Xcode 的圖標菜單并選擇 Open Developer Tool 選項,點擊 Accessibility Inspector 即可開始使用。
當我們沒有設置 isAccessibilityElement 屬性時,在 Accessibility 元素層級結構中就無法看到 UIImageView 和 UIView 元素,只能看到 “t我是Button” 和“我是Label”。而當我們將 UIView 的 isAccessibilityElement 屬性設置為 YES 時, UIView 元素才能在元素層級中可見,UIImageView默認還是看不見。設置代碼如下:
NSArray *nameArr = @[@"我是Button", @"我是Label", @"我是View", @"我是Image"];
if (i == 0) { // 按鈕
UIButton *btn = [[UIButton alloc] initWithFrame:btnF];
[btn setTitle:nameArr[i] forState:UIControlStateNormal];
temView = btn;
} else if (i == 1) { // label
UILabel *lab = [[UILabel alloc] initWithFrame:btnF];
lab.text = nameArr[i];
lab.textAlignment = NSTextAlignmentCenter;
temView = lab;
} else if (i == 2) { // view
UIView *view = [[UIView alloc] initWithFrame:btnF];
view.isAccessibilityElement = YES; // 將 UIView 的 isAccessibilityElement 屬性設置為 YES
view.accessibilityIdentifier = nameArr[I];
temView = view;
} else if (i == 3) { // 圖片
UIImageView *imgView = [[UIImageView alloc] initWithFrame:btnF];
imgView.image = [UIImage imageNamed:@"avatar"];
imgView.accessibilityIdentifier = nameArr[i];
temView = imgView;
}
5.2、Accessibility 相關屬性
@property (nullable, nonatomic, copy) NSString *accessibilityLabel;
accessibilityLabel 屬性可以解決絕大部分的 Accessibility 問題,當光標將焦點放在設置該屬性的元素師時,它的內容可由 VoiceOver 讀取的人類可讀的字符串。但如果不是需要被視障用戶獲知的視圖元素,僅用于自動化測試,就可以不用設置該屬性。
@property(nullable, nonatomic, copy) NSString *accessibilityIdentifier API_AVAILABLE(ios(5.0));
accessibilityIdentifier 屬性不會被 VoiceOver 誦讀,而是面向開發人員的字符串,可在不希望用戶操作 accessibilityLabel 的情況下使用。
@property (nonatomic) BOOL isAccessibilityElement;
如果 isAccessibilityElement 未設置為 true,那么這個視圖將不會在 Accessibility 視圖層次結構中可見。
- The default value for this property is false unless the element is a standard UIKit control, in which case, the value is true. —— Apple Documentation
另外,根據 Apple 官方中的介紹 UIControl 的子類的 isAccessibilityElement 屬性都默認設置為 true。
5.3、編寫測試用例
- (void)testExample {
XCUIApplication *app = [[XCUIApplication alloc] init];
[app.staticTexts[@"Accessibility Demo"] tap];
XCUIElement *button = app.buttons[@"我是Button"];
XCTAssertTrue(button.exists);
XCUIElement *label = app.staticTexts[@"我是Label"];
XCTAssertTrue(label.exists);
XCUIElement *view = app.otherElements[@"我是View"];
XCTAssertTrue(view.exists);
XCUIElement *imgview = app.images[@"我是Image"];
XCTAssertTrue(imgview.exists);
}
六、三方框架KIF簡介
6.1、KIF簡介
KIF 的全稱是Keep it functional。它是一個建立在XCTest的UI測試框架,通過accessibility來定位具體的控件,再利用私有的API來操作UI。由于是建立在XCTest上的,所以你可以完美的借助XCode的測試相關工具。
6.2、pod引入框架
必須將Target設置為Unit Test,根據GitHub官方說明。不要設置成UI Test 項目了,我這就設錯了,調了大半天才找到原因
查看GitHub的ReadMe,使用Cocoapod進行安裝,命令如下(在Debug模式下才生效)
-
KIF一定要放到測試項目下面
target 'ZJHKIFUnitTestDemoTests' do pod 'KIF', :configurations => ['Debug'] end
6.3、簡單使用
更多接口介紹,可參考:KIF API中文翻譯
參考鏈接:
iOS 單元測試和 UI 測試快速入門:https://juejin.cn/post/6844903744170098695
iOS UI 自動化測試原理以及在 Trip.com 的應用實踐:https://www.51cto.com/article/686176.html
iOS UI Testing 指北:https://nixwang.com/2018/09/30/ios-ui-testing/