基于 KIF 的 iOS UI 自動化測試和持續集成
客戶端 UI 自動化測試是大多數測試團隊的研究重點,本文介紹貓眼測試團隊在貓眼 iOS 客戶端實踐的基于 KIF 的 UI 自動化測試和持續集成過程。
一、測試框架的選擇
iOS UI 自動化測試框架有不少,其中 UI Automation 是 Apple 早期提供的 UI 自動化測試解決方法,用 JavaScript 編寫測試腳本,通過標簽和值的可訪問性獲得 UI 元素,來完成相應的交互操作。
一些第三方 UI 解決方案以 UI Automation 為基礎,對其進行補充和優化,包括擴展型 UI Automation 和驅動型 UI Automation。
- 擴展型 UI Automation 采用 JavaScript 擴展庫方法提高 UI Automation 的易用性,常見的框架有 TuneupJs、ynm3k。
- 驅動型 UI Automation 在自動化測試底層使用了 UI Automation 庫,通過 TCP 等通信方式驅動 UI Automation 來完成自動化測試。這種方式下,編輯腳本的語言不再局限于 JavaScript 。常見的框架有 iOSDriver、Appium。
還有一些其他的第三方解決方案,常見的框架類型有私有 API 型和注入編譯型。
- 私有 API 型框架直接使用 Apple 私有 API 對 UI 界面進行操作。常見的框架主要有 KIF。
- 注入編譯型框架在編譯時注入一個 Server 到 App 內部,通過 Server 對外通信完成 UI 操作指令的執行。常見的框架有 Frank、Calabash。
Xcode 7發布后,Apple 提供了一種新的 UI 自動化測試解決方法——UI Testing,它基于 XCTest 測試框架,通過控件的可訪問性來定位和獲取控件,并提供了多種 UI 操作 API,使用源碼語言,能方便地進行調試。
我們在以上分類中挑選具有代表性的自動化框架:UI Automation、Appium、KIF、Frank、UI Testing 進行對比,下表是這幾種測試框架的特點對比:
考慮選擇測試框架的幾種影響因素。首先,使用的語言和框架決定了測試人員的持續性學習成本,iOS 測試人員對 Object C 和 XCTest 熟悉和掌握程度高,不需要消耗額外的學習成本,人員更替時的接手成本也相對較低;其次,測試框架支持的 UI 操作的豐富性決定了測試用例的覆蓋完整度,使用私有 API 的測試框架支持的 UI 操作較為全面,而同時支持 UIWebView 的測試框架則更占優勢;另外,App 程序 UI 變化快,使用開發效率高、調試方便的測試框架能使我們在適應新 UI 變化、新需求時獲得更小的投入產出比。
綜合以上考慮,KIF 框架已經展現了他的優勢,并且 KIF 使用 XCTest 框架,使得其測試流程 iOS 程序的單測無異,可完全復用單測的持續集成流程,維護持續集成的成本相對降低;另外,KIF 是一個活躍的開源測試框架,可擴展性好,升級更新快,有活躍社區來探討和解決使用過程中遇到的問題。鑒于上述優勢,我們選擇了 KIF 作為 iOS 的 UI 自動化測試框架。
二、KIF 自動化實施
KIF 利用 Apple 給所有控件提供的輔助屬性 accessibility attributes 來定位和獲取元素,完成界面的交互操作;結合使用 Xcode 的 XCTest 測試框架,擁有 XCTest 測試框架的特性,使得測試用例能以 command line build 工具運行并獲取測試報告。
下面介紹如何進行 KIF 自動化實施。
1. KIF 搭建
KIF 以第三方庫的形式編譯運行于工程中,搭建 KIF 之前,應該確保工程在 Xcode 上編譯運行通過。
KIF 基于 XCTest 框架,繼承了 XCTest 的所有特性。和 XCTest 一樣,我們首先應該在工程項目中創建基于 Cocoa Touch Testing Bundle 模板的 Target ,并確保創建的 Target 的屬性有如下設置:
- “Build Phases”:設置 Target Dependencies , UI 自動化測試固然要依賴應用程序的 App 產物,所以需保證應用程序 Target 被添加在 Test Target 的 Target Dependencies 中。
- “Build Settings”:
設置 “Bundle loader” 為:$(BUILT_PRODUCTS_DIR)/MyApp.app/MyApp;
設置 “Test Host” 為:$(BUILT_PRODUCTS_DIR);
設置 “Wrapper Extensions” 為:xctest。
項目的設置準備好后,需要安裝 KIF 庫源碼到項目。即可開始 KIF 編寫用例之旅。
KIF 通過屬性值(AccessibilityLabel, AccessibilityIdentifier, AccessibilityTraits,Value...)在界面中定位元素。為了獲取到目標元素,我們必須先設置元素的 accessibility 屬性。如下,想要獲取程序中一個列表的 cell 元素,我們給列表的 cell 控件設置 accessibility 屬性(如左圖所示),設置為“Section XX Row XX”,編譯運行,即可獲得歷史列表的 cell 元素;用模擬器的 Accessibility Inspector 抓取到了這個歷史列表元素(如右圖所示):
KIF 為我們提供了對有 accessibility 屬性控件的操作接口,如下最簡單的兩個操作接口:
- 點擊一個元素:- (void)tapViewWithAccessibilityLabel:(NSString *)label;
- 等待一個元素的出現:- (UIView *)waitForViewWithAccessibilityLabel:(NSString *)label。
在新建的 Target 同名目錄下增加一個繼承自 KIFTestCase 的類,類中編寫我們的用例,完成對界面的點擊和驗證,如下:
以上步驟都完成后, 基于KIF的簡單用例便搭建完成,點擊 Product->Test 或者快捷鍵 (?U) 即可看到我們的用例自動運行起來了。
2. 用例編寫與組織
(1)accessibility 屬性設置
accessibility 屬性是 Apple 給視覺障礙人群提供完全無障礙使用的基本屬性,該屬性表明了 UI 元素的可訪問性、是什么、做什么以及會觸發什么樣的操作。原生的 UIKit 控件默認提供了這些信息,然而,自定義的控件則需要對該屬性進行設置,設置方式可參考下面幾點:
- 設置方式:找到頁面元素所屬的代碼文件,再到代碼中找到該類的實現,在相應代碼處添加其屬性。
- 查看方式:設置好后,開啟模擬器的 Accessibility Inspector 功能,即可看到控件的 accessibility 屬性。
- 設置建議:設置的 AccessibilityLabel 屬性值要有實際意義(用戶可理解),因為設置這個屬性后用戶可以通過 VoiceOver 訪問;用戶不可訪問的控件,比如某些放置控件的容器等應該設置為 AccessibilityIdentifier 。
(2)用例常用操作接口:
- UI交互操作( KIFUITestActor.h 中可查閱):
tapThisView: - (void)tapViewWithAccessibilityLabel:(NSString *)label;
waitForView: - (UIView *)waitForViewWithAccessibilityLabel:(NSString *)label;
注意:函數返回了對應View的指針,可以對返回值取數據,從而進行一些判斷
enterTextIntoView: - (void)enterText:(NSString *)text intoViewWithAccessibilityLabel:(NSString *)label;
tapRowOnTableView: - (void)tapRowAtIndexPath:(NSIndexPath *)indexPath inTableViewWithAccessibilityIdentifier:(NSString *)identifier NS_AVAILABLE_IOS(5_0);
dismisses a system alert: - (void)acknowledgeSystemAlert;
擴展:我們還可以對 KIFUITestActor 類進行擴展,利用 KIFUITestActor 中的私有函數,使 AccessibilityIdentifier 代替 Label 識別元素,完成 tapThisView 、waitForView 等操作。
- 用例集操作( KIFTestCase.h 中可查閱):
- (void)beforeAll; 在本類中第一個 test case執行前執行一次
用處:執行本類中各個測試函數的公共操作
注意:因為不能保證這個方法與 test case 是同一個類實例,所以不能用來設置實例變量的值,但是可以設置靜態變量
- (void)beforeEach; 在每一個 test case 執行前執行一次
用處:執行各個函數需要的測試環境
注意:因為確保這個方法與 test case 是同一個類實例,所以可以用來設置實例變量
- (void)afterEach; 在每一個 test case執行后執行一次
用處:用來將 App 恢復至 test case 之前的狀態,可以包含一些條件判斷邏輯,從失敗的 test case 中恢復,以確保不影響之后的測試
- (void)afterAll; 執行完測試類的最后一個 test case 后執行一次
用處:用于將 App 恢復至測試的初始狀態
- 系統的功能實現( KIFSystemTestActor.h 中可查閱):
模擬用戶旋轉設備: - (void)simulateDeviceRotationToOrientation:(UIDeviceOrientation)orientation;
對當前屏幕截圖并存儲到硬盤中:- (void)captureScreenshotWithDescription:(NSString *)description;
(3)用例組織
設計實現單個測試用例步驟如下:
- a. 設置測試所需要的環境;
- b. 測試用例的測試邏輯;
- c. 恢復App至此次測試前狀態。
a、c步驟可用 beforeEach、afterEach 來實現,這樣保證了每個用例之間的獨立性和用例運行的穩定性。
一般來說,可將用例按功能分成若干個用例集,每個用例集按校驗點或者功能點分成若干個用例,這樣方便測試用例的管理和維護。 某些含有耗費時間多、耗費資源多的公共操作的用例可以集合成一個用例集,在用例集運行前統一執行。設計實現用例集步驟如下:
- a. 設置用例集需要的環境、公共操作;
- b. 設計各個用例;
- c. 恢復 App 至用例集測試的初始狀態。
a、c步驟可用 beforeAll、afterAll 來實現,下圖展示了一個用例集的書寫示例:
#import "TimerTests.h"
#import "KIFUITestActor+AccessibilityLabelAddition.h"
#import "KIFUITestActor+IdentifierAdditions.h"
#import "KIFUITestActor+TimerAdditions.h"
@implementation TimerTests
- (void)beforeAll
{
[tester setDebugModel];
}
- (void)afterAll
{
[tester resetDebugModel];
[tester clearHistory];
}
- (void)beforeEach
{
[tester setDebugModel];
}
- (void)afterEach
{
[tester clearParams];
}
- (void)testNameedTask
{
[tester enterText:@"myTask" intoViewWithAccessibilityLabel:@"Task Name Input"];
[tester enterWorktime:10 Breaktime:4 Repetitions:5];
[tester tapViewWithAccessibilityLabel:@"Start Working"];
[tester waitForViewWithAccessibilityLabel:@"myTask"];
[tester waitForViewWithAccessibilityLabel:@"Start Working"];
}
- (void)testnoNameTask
{
[tester enterWorktime:10 Breaktime:4 Repetitions:5];
[tester tapViewWithAccessibilityLabel:@"Start Working"];
[tester waitForViewWithAccessibilityLabel:@"myTask"];
[tester waitForViewWithAccessibilityLabel:@"Start Working"];
}
- (void)testPresetTask
{
[tester tapViewWithAccessibilityLabel:@"Presets"];
[tester tapRowAtIndexPath:@"Classic" inTableViewWithAccessibilityIdentifier:@"Presets List"];
[tester tapViewWithAccessibilityLabel:@"Start Working"];
[tester waitForViewWithAccessibilityLabel:@"myTask"];
[tester waitForViewWithAccessibilityLabel:@"Start Working"];
}
@end
上述代碼中,我們看到許多封裝函數。為保證用例結構清晰明朗,我們借鑒 selenium pageObject 的設計方式, 遵循如下規則:
- a. 將頁面上的對元素的發現、操作處理抽象為相應的類,返回操作結果;
- b. 封裝盡可能多的工具類;
- c. 測試用例只關注用例邏輯,步驟盡量簡潔。
如下圖所示,在用例集 test suite 中,我們只保持清晰的用例邏輯;非用例邏輯的動作封裝成相應地用例集的類 test suite additions ;因為 KIF 的開源性,我們還可以利用 KIF 的私有 API 封裝我們需要的工具 Tools 類。
(4)用例的運行獨立和 retry 機制
失敗用例是不可避免的,上述用例的組織方式,降低了用例間的依賴性,但是并不能完全消除失敗用例對后續用例執行的影響。如果能讓每個用例獨立啟動 App 執行 case,則能保證后執行用例不受先執行失敗用例的影響。如果在 case 運行失敗后,還可以進行 retry 重試,則能提高用例運行的穩定性。xctool 工具能給我們帶來這樣的功能,我們用 xctool 命令先 build-tests 構建 app,然后循環啟動 app 來 run-tests 用例,用例失敗后,重新執行。下面是一個 xctool 獨立運行用例的簡單示例:
xctool build-tests -workspace myApp.xcworkspace -scheme myKIFTestScheme -sdk iphonesimulator -configuration Debug -destination platform='iOS Simulator',OS=8.3,name='iPhone 6 Plus'
array=( TimerTests HistoryTests )
for data in ${array[@]}
do
xctool -reporter pretty -reporter junit:tmp/test-report-tmp.xml -workspace myApp.xcworkspace -scheme myKIFTestScheme run-tests -only myKIFTestTarget:${data} -sdk iphonesimulator -configuration Debug -destination platform='iOS Simulator',OS=8.3,name='iPhone 6 Plus'
done
三、KIF 自動化的持續集成
1. 持續集成的意義與 UI 自動化測試的用例選擇
持續集成是一個自動化的周期性的集成測試過程,從檢出代碼、編譯構建、運行測試、結果記錄、測試統計等都是自動完成的,無需人工干預。我們的項目都是團隊協作開發,采用持續集成的優勢顯而易見:
- 盡早盡快地發現集成錯誤,保證團隊開發人員提交代碼的質量,減輕軟件發布時的壓力;
- 自動完成集成中的環節,有利于減少集成過程的重復工作以節省時間、費用和工作量;
持續集成最大的好處在于能夠盡早高效發現問題,降低解決問題的成本。而發現問題的手段主要就是測試。
根據 Martin Fowler 的測試理論,測試應該遵循如下測試金字塔組合,測試金字塔最底層是單元測試,然后是集成測試,繼而是面向應用程序服務層的中間層測試,最高層是面向用戶的業務邏輯測試:
測試自動化的測試層級越多,持續集成平臺就能產生越大的價值。
UI 測試目標是覆蓋最核心的代碼,盡可能去掉依賴,讓不穩定因子降到最低,這樣既保證自動化測試層級的全面性,又保證持續集成的穩定構建,降低測試的投入產出比。因此,在我們的 UI 自動化測試中,我們選擇核心功能的冒煙用例來完成持續集成中的測試金字塔。
2. Jenkins 上完成基于 KIF 的 UI 自動化持續集成搭建
Jenkins 是一個開源的持續集成工具,提供了一種易于使用的持續集成系統,使開發者從繁雜的集成中解脫出來,專注于更為重要的業務邏輯實現上。
Jenkins 以 Job 為單位運行項目,一個 Job 的工作流程為:在指定的時機,選擇合適的 salve 節點,從版本管理系統上獲取對應的源碼,使用命令行腳本或者 maven 或者 ant 進行構建,構建后歸檔文件,處理報告,如果構建失敗那么就通過郵件進行反饋等。
Job 的觸發時機主要有3種選擇:
- "Build after other project are build":表示在其他某個項目build后觸發,比如我們可以在某個提測Job構建之后,立即構建我們的 UI 自動化來驗證這個提測的可行性;
- "Build periodically":表示按時間觸發,我們可以選擇這個讓 Job 做 Daily Build 來進行持續構建觀察;
- "Poll SCM":表示允許用戶讓 Jenkins 定期查詢某一個項目的代碼庫,如果有代碼變動則觸發執行任務,這種觸發非常適合集成測試項目,以此驗證代碼庫變動是否能測試通過。
我們希望在代碼改動發生的時候就做到盡早發現代碼改動帶來的問題,所以使用 “Poll SCM” 在當代碼倉庫有新的 pull request 的時候觸發相應 Job 完成構建,Job 的執行結果作為這個 pull request 能否合入的衡量指標之一;同時為支持客戶端支持 daily build ,Job 使用 "Build periodically" 在每天 daily build 打包前完成一次自動構建。
Job 需要支持命令行構建才能實現持續集成,如上一部分提到,我們可以借助 xcodebuild/xctool 實現單命令行構建。同時為了衡量 Job 的執行結果,我們需要在 Job 執行完成后生成相應的測試報告和代碼覆蓋率報告,使用 xcodebuild/xctool 這樣的命令行工具,只需要配置相關的參數即可獲取相應的 XML 測試報告文件。
Jenkins 中 JUnit Plugin 插件可以將 XML 形式的測試報告轉化成一種隨時間推移的測試結果圖表,向我們展示測試的結果和測試的穩定性; Cobertura plugin 插件可以將 XML 形式的覆蓋率文件轉化成一種隨時間推移的代碼覆蓋率圖表。如下圖是 Job 中測試報告的代碼覆蓋率和測試結果的示例,通過下面的圖表,我們可以清晰地看到測試是否通過,檢查代碼的測試覆蓋范圍,并對比歷史的測試結果和代碼覆蓋率來推斷和定位問題。
3. KIF 自動化測試在 Jenkins 持續集成過程中遇到的問題
(1)設備重置
我們的測試用例覆蓋了第一次安裝啟動的操作。在初期,這個用例經常失敗。經過排查發現,持續集成系統中的模擬器設備重置操作并沒有覆蓋所有的設備,UI 測試 Job 運行時,Job 選擇的模擬器設備上可能遺留了其他 Job 構建的相同的 app 產物,導致我們的 Job 構建產物并不是第一次安裝啟動。所以在腳本中我們遍歷所有模擬器設備,將其進行重置。
(2)鍵盤敲擊延遲
我們的測試用例在輸入框輸入文字時,經常出現輸入不全而導致失敗的問題。比如在輸入框中輸入 'beijing' ,失敗后提示:Failed to get text in field; instead, it was 'beiji' 。經過排查,發現持續集成系統中的機器性能有高有低,在低性能機器中更容易發生此問題,再研究 KIF 框架源碼發現,KIF 默認設置的鍵盤敲擊時延為一個常數,對于低性能機器來說這個敲擊時延較短,容易漏掉輸入,所以我們在 KIFTypist.m 源碼文件中適當增加 (NSTimeInterval) keystrokeDelay 的時長來避免輸入不全的問題。
(3)多個系統彈窗確認
前面我們提到過,KIF 支持對系統彈窗的處理,即接口 acknowledgeSystemAlert ,它能幫我們確認一個系統彈窗。但是我們的應用程序在啟動時系統彈窗并不止一個,并且在不同設備上,因系統設置不同,系統彈窗的個數是不確定的。所以,直接使用 acknowledgeSystemAlert 并不能幫我們解決問題。因為 KIF 的開源性,我們在 KIF 框架源碼 acknowledgeSystemAlert 函數中做了一次 while 循環處理,處理了出現的任意多個系統彈窗的情況,從而解決了問題。
參考文獻:
- Automate UI Testing in iOS: https://developer.apple.com/library/tvos/documentation/DeveloperTools/Conceptual/InstrumentsUserGuide/UIAutomation.html
- Appium 官網介紹: http://appium.io/slate/cn/v1.2.0/?ruby#appium
- Frank 官網介紹: http://www.testingwithfrank.com/
- KIF 源碼庫: https://github.com/kif-framework/KIF
- iOS UI Testing with KIF: http://www.raywenderlich.com/61419/ios-ui-testing-with-kif
- The current state of iOS automated functional testing: http://watirmelon.com/2013/11/04/the-current-state-of-ios-automated-functional-testing/
- Page Object: http://martinfowler.com/bliki/PageObject.html
- Test Pyramid: http://martinfowler.com/bliki/TestPyramid.html
- Continuous Integration: http://www.martinfowler.com/articles/continuousIntegration.html
- xcodebuild: https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man1/xcodebuild.1.html
- xctool: https://github.com/非死book/xctool
- Jenkins 官網介紹: https://wiki.jenkins-ci.org/display/JENKINS/Home
- JUnit Plugin: https://wiki.jenkins-ci.org/display/JENKINS/JUnit+Plugin
- Cobertura plugin: https://wiki.jenkins-ci.org/display/JENKINS/Cobertura+Plugin
- Xcode 7 UI Testing: https://developer.apple.com/videos/play/wwdc2015/406/
來自:http://tech.meituan.com/iOS-UITest-KIF.html