基于 KIF 的 iOS UI 自动化测试和持续集成
客戶端 UI 自動(dòng)化測(cè)試是大多數(shù)測(cè)試團(tuán)隊(duì)的研究重點(diǎn),本文介紹貓眼測(cè)試團(tuán)隊(duì)在貓眼 iOS 客戶端實(shí)踐的基于 KIF 的 UI 自動(dòng)化測(cè)試和持續(xù)集成過(guò)程。
一、測(cè)試框架的選擇
iOS UI 自動(dòng)化測(cè)試框架有不少,其中 UI Automation 是 Apple 早期提供的 UI 自動(dòng)化測(cè)試解決方法,用 JavaScript 編寫(xiě)測(cè)試腳本,通過(guò)標(biāo)簽和值的可訪問(wèn)性獲得 UI 元素,來(lái)完成相應(yīng)的交互操作。
一些第三方 UI 解決方案以 UI Automation 為基礎(chǔ),對(duì)其進(jìn)行補(bǔ)充和優(yōu)化,包括擴(kuò)展型 UI Automation 和驅(qū)動(dòng)型 UI Automation。
- 擴(kuò)展型 UI Automation 采用 JavaScript 擴(kuò)展庫(kù)方法提高 UI Automation 的易用性,常見(jiàn)的框架有 TuneupJs、ynm3k。
- 驅(qū)動(dòng)型 UI Automation 在自動(dòng)化測(cè)試底層使用了 UI Automation 庫(kù),通過(guò) TCP 等通信方式驅(qū)動(dòng) UI Automation 來(lái)完成自動(dòng)化測(cè)試。這種方式下,編輯腳本的語(yǔ)言不再局限于 JavaScript 。常見(jiàn)的框架有 iOSDriver、Appium。
還有一些其他的第三方解決方案,常見(jiàn)的框架類(lèi)型有私有 API 型和注入編譯型。
- 私有 API 型框架直接使用 Apple 私有 API 對(duì) UI 界面進(jìn)行操作。常見(jiàn)的框架主要有 KIF。
- 注入編譯型框架在編譯時(shí)注入一個(gè) Server 到 App 內(nèi)部,通過(guò) Server 對(duì)外通信完成 UI 操作指令的執(zhí)行。常見(jiàn)的框架有 Frank、Calabash。
Xcode 7發(fā)布后,Apple 提供了一種新的 UI 自動(dòng)化測(cè)試解決方法——UI Testing,它基于 XCTest 測(cè)試框架,通過(guò)控件的可訪問(wèn)性來(lái)定位和獲取控件,并提供了多種 UI 操作 API,使用源碼語(yǔ)言,能方便地進(jìn)行調(diào)試。 我們?cè)谝陨戏诸?lèi)中挑選具有代表性的自動(dòng)化框架:UI Automation、Appium、KIF、Frank、UI Testing 進(jìn)行對(duì)比,下表是這幾種測(cè)試框架的特點(diǎn)對(duì)比:
考慮選擇測(cè)試框架的幾種影響因素。首先,使用的語(yǔ)言和框架決定了測(cè)試人員的持續(xù)性學(xué)習(xí)成本,iOS 測(cè)試人員對(duì) Objective—C 和 XCTest 熟悉和掌握程度高,不需要消耗額外的學(xué)習(xí)成本,人員更替時(shí)的接手成本也相對(duì)較低;其次,測(cè)試框架支持的 UI 操作的豐富性決定了測(cè)試用例的覆蓋完整度,使用私有 API 的測(cè)試框架支持的 UI 操作較為全面,而同時(shí)支持 UIWebView 的測(cè)試框架則更占優(yōu)勢(shì);另外,App 程序 UI 變化快,使用開(kāi)發(fā)效率高、調(diào)試方便的測(cè)試框架能使我們?cè)谶m應(yīng)新 UI 變化、新需求時(shí)獲得更小的投入產(chǎn)出比。 綜合以上考慮,KIF 框架已經(jīng)展現(xiàn)了他的優(yōu)勢(shì),并且 KIF 使用 XCTest 框架,使得其測(cè)試流程 iOS 程序的單測(cè)無(wú)異,可完全復(fù)用單測(cè)的持續(xù)集成流程,維護(hù)持續(xù)集成的成本相對(duì)降低;另外,KIF 是一個(gè)活躍的開(kāi)源測(cè)試框架,可擴(kuò)展性好,升級(jí)更新快,有活躍社區(qū)來(lái)探討和解決使用過(guò)程中遇到的問(wèn)題。鑒于上述優(yōu)勢(shì),我們選擇了 KIF 作為 iOS 的 UI 自動(dòng)化測(cè)試框架。
二、KIF 自動(dòng)化實(shí)施
KIF 利用 Apple 給所有控件提供的輔助屬性 accessibility attributes 來(lái)定位和獲取元素,完成界面的交互操作;結(jié)合使用 Xcode 的 XCTest 測(cè)試框架,擁有 XCTest 測(cè)試框架的特性,使得測(cè)試用例能以 command line build 工具運(yùn)行并獲取測(cè)試報(bào)告。
下面介紹如何進(jìn)行 KIF 自動(dòng)化實(shí)施。
1. KIF 搭建
KIF 以第三方庫(kù)的形式編譯運(yùn)行于工程中,搭建 KIF 之前,應(yīng)該確保工程在 Xcode 上編譯運(yùn)行通過(guò)。 KIF 基于 XCTest 框架,繼承了 XCTest 的所有特性。和 XCTest 一樣,我們首先應(yīng)該在工程項(xiàng)目中創(chuàng)建基于 Cocoa Touch Testing Bundle 模板的 Target ,并確保創(chuàng)建的 Target 的屬性有如下設(shè)置:
- “Build Phases”:設(shè)置 Target Dependencies , UI 自動(dòng)化測(cè)試固然要依賴(lài)應(yīng)用程序的 App 產(chǎn)物,所以需保證應(yīng)用程序 Target 被添加在 Test Target 的 Target Dependencies 中。
- “Build Settings”: ????設(shè)置 “Bundle loader” 為:$(BUILT_PRODUCTS_DIR)/MyApp.app/MyApp; ????設(shè)置 “Test Host” 為:$(BUILT_PRODUCTS_DIR); ????設(shè)置 “Wrapper Extensions” 為:xctest。
項(xiàng)目的設(shè)置準(zhǔn)備好后,需要安裝 KIF 庫(kù)源碼到項(xiàng)目。即可開(kāi)始 KIF 編寫(xiě)用例之旅。 KIF 通過(guò)屬性值(AccessibilityLabel, AccessibilityIdentifier, AccessibilityTraits,Value…)在界面中定位元素。為了獲取到目標(biāo)元素,我們必須先設(shè)置元素的 accessibility 屬性。如下,想要獲取程序中一個(gè)列表的 cell 元素,我們給列表的 cell 控件設(shè)置 accessibility 屬性(如左圖所示),設(shè)置為“Section XX Row XX”,編譯運(yùn)行,即可獲得歷史列表的 cell 元素;用模擬器的 Accessibility Inspector 抓取到了這個(gè)歷史列表元素(如右圖所示):
KIF 為我們提供了對(duì)有 accessibility 屬性控件的操作接口,如下最簡(jiǎn)單的兩個(gè)操作接口:
- 點(diǎn)擊一個(gè)元素:- (void)tapViewWithAccessibilityLabel:(NSString *)label;
- 等待一個(gè)元素的出現(xiàn):- (UIView *)waitForViewWithAccessibilityLabel:(NSString *)label。
在新建的 Target 同名目錄下增加一個(gè)繼承自 KIFTestCase 的類(lèi),類(lèi)中編寫(xiě)我們的用例,完成對(duì)界面的點(diǎn)擊和驗(yàn)證,如下:
以上步驟都完成后, 基于KIF的簡(jiǎn)單用例便搭建完成,點(diǎn)擊 Product->Test 或者快捷鍵 (?U) 即可看到我們的用例自動(dòng)運(yùn)行起來(lái)了。
2. 用例編寫(xiě)與組織
(1)accessibility 屬性設(shè)置
accessibility 屬性是 Apple 給視覺(jué)障礙人群提供完全無(wú)障礙使用的基本屬性,該屬性表明了 UI 元素的可訪問(wèn)性、是什么、做什么以及會(huì)觸發(fā)什么樣的操作。原生的 UIKit 控件默認(rèn)提供了這些信息,然而,自定義的控件則需要對(duì)該屬性進(jìn)行設(shè)置,設(shè)置方式可參考下面幾點(diǎn):
- 設(shè)置方式:找到頁(yè)面元素所屬的代碼文件,再到代碼中找到該類(lèi)的實(shí)現(xiàn),在相應(yīng)代碼處添加其屬性。
- 查看方式:設(shè)置好后,開(kāi)啟模擬器的 Accessibility Inspector 功能,即可看到控件的 accessibility 屬性。
- 設(shè)置建議:設(shè)置的 AccessibilityLabel 屬性值要有實(shí)際意義(用戶可理解),因?yàn)樵O(shè)置這個(gè)屬性后用戶可以通過(guò) VoiceOver 訪問(wèn);用戶不可訪問(wèn)的控件,比如某些放置控件的容器等應(yīng)該設(shè)置為 AccessibilityIdentifier 。
(2)用例常用操作接口:
- UI交互操作( KIFUITestActor.h 中可查閱):
擴(kuò)展:我們還可以對(duì) KIFUITestActor 類(lèi)進(jìn)行擴(kuò)展,利用 KIFUITestActor 中的私有函數(shù),使 AccessibilityIdentifier 代替 Label 識(shí)別元素,完成 tapThisView 、waitForView 等操作。
- 用例集操作( KIFTestCase.h 中可查閱):
- 系統(tǒng)的功能實(shí)現(xiàn)( KIFSystemTestActor.h 中可查閱):
(3)用例組織
設(shè)計(jì)實(shí)現(xiàn)單個(gè)測(cè)試用例步驟如下: * a. 設(shè)置測(cè)試所需要的環(huán)境; * b. 測(cè)試用例的測(cè)試邏輯; * c. 恢復(fù)App至此次測(cè)試前狀態(tài)。
a、c步驟可用 beforeEach、afterEach 來(lái)實(shí)現(xiàn),這樣保證了每個(gè)用例之間的獨(dú)立性和用例運(yùn)行的穩(wěn)定性。 一般來(lái)說(shuō),可將用例按功能分成若干個(gè)用例集,每個(gè)用例集按校驗(yàn)點(diǎn)或者功能點(diǎn)分成若干個(gè)用例,這樣方便測(cè)試用例的管理和維護(hù)。 某些含有耗費(fèi)時(shí)間多、耗費(fèi)資源多的公共操作的用例可以集合成一個(gè)用例集,在用例集運(yùn)行前統(tǒng)一執(zhí)行。設(shè)計(jì)實(shí)現(xiàn)用例集步驟如下:
- a. 設(shè)置用例集需要的環(huán)境、公共操作;
- b. 設(shè)計(jì)各個(gè)用例;
- c. 恢復(fù) App 至用例集測(cè)試的初始狀態(tài)。
a、c步驟可用 beforeAll、afterAll 來(lái)實(shí)現(xiàn),下圖展示了一個(gè)用例集的書(shū)寫(xiě)示例:
#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上述代碼中,我們看到許多封裝函數(shù)。為保證用例結(jié)構(gòu)清晰明朗,我們借鑒 selenium pageObject 的設(shè)計(jì)方式, 遵循如下規(guī)則:
- a. 將頁(yè)面上的對(duì)元素的發(fā)現(xiàn)、操作處理抽象為相應(yīng)的類(lèi),返回操作結(jié)果;
- b. 封裝盡可能多的工具類(lèi);
- c. 測(cè)試用例只關(guān)注用例邏輯,步驟盡量簡(jiǎn)潔。
如下圖所示,在用例集 test suite 中,我們只保持清晰的用例邏輯;非用例邏輯的動(dòng)作封裝成相應(yīng)地用例集的類(lèi) test suite additions ;因?yàn)?KIF 的開(kāi)源性,我們還可以利用 KIF 的私有 API 封裝我們需要的工具 Tools 類(lèi)。
(4)用例的運(yùn)行獨(dú)立和 retry 機(jī)制
失敗用例是不可避免的,上述用例的組織方式,降低了用例間的依賴(lài)性,但是并不能完全消除失敗用例對(duì)后續(xù)用例執(zhí)行的影響。如果能讓每個(gè)用例獨(dú)立啟動(dòng) App 執(zhí)行 case,則能保證后執(zhí)行用例不受先執(zhí)行失敗用例的影響。如果在 case 運(yùn)行失敗后,還可以進(jìn)行 retry 重試,則能提高用例運(yùn)行的穩(wěn)定性。xctool 工具能給我們帶來(lái)這樣的功能,我們用 xctool 命令先 build-tests 構(gòu)建 app,然后循環(huán)啟動(dòng) app 來(lái) run-tests 用例,用例失敗后,重新執(zhí)行。下面是一個(gè) xctool 獨(dú)立運(yùn)行用例的簡(jiǎn)單示例:
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[@]} doxctool -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 自動(dòng)化的持續(xù)集成
1. 持續(xù)集成的意義與 UI 自動(dòng)化測(cè)試的用例選擇
持續(xù)集成是一個(gè)自動(dòng)化的周期性的集成測(cè)試過(guò)程,從檢出代碼、編譯構(gòu)建、運(yùn)行測(cè)試、結(jié)果記錄、測(cè)試統(tǒng)計(jì)等都是自動(dòng)完成的,無(wú)需人工干預(yù)。我們的項(xiàng)目都是團(tuán)隊(duì)協(xié)作開(kāi)發(fā),采用持續(xù)集成的優(yōu)勢(shì)顯而易見(jiàn):
- 盡早盡快地發(fā)現(xiàn)集成錯(cuò)誤,保證團(tuán)隊(duì)開(kāi)發(fā)人員提交代碼的質(zhì)量,減輕軟件發(fā)布時(shí)的壓力;
- 自動(dòng)完成集成中的環(huán)節(jié),有利于減少集成過(guò)程的重復(fù)工作以節(jié)省時(shí)間、費(fèi)用和工作量;
持續(xù)集成最大的好處在于能夠盡早高效發(fā)現(xiàn)問(wèn)題,降低解決問(wèn)題的成本。而發(fā)現(xiàn)問(wèn)題的手段主要就是測(cè)試。 根據(jù) Martin Fowler 的測(cè)試?yán)碚?#xff0c;測(cè)試應(yīng)該遵循如下測(cè)試金字塔組合,測(cè)試金字塔最底層是單元測(cè)試,然后是集成測(cè)試,繼而是面向應(yīng)用程序服務(wù)層的中間層測(cè)試,最高層是面向用戶的業(yè)務(wù)邏輯測(cè)試:
測(cè)試自動(dòng)化的測(cè)試層級(jí)越多,持續(xù)集成平臺(tái)就能產(chǎn)生越大的價(jià)值。 UI 測(cè)試目標(biāo)是覆蓋最核心的代碼,盡可能去掉依賴(lài),讓不穩(wěn)定因子降到最低,這樣既保證自動(dòng)化測(cè)試層級(jí)的全面性,又保證持續(xù)集成的穩(wěn)定構(gòu)建,降低測(cè)試的投入產(chǎn)出比。因此,在我們的 UI 自動(dòng)化測(cè)試中,我們選擇核心功能的冒煙用例來(lái)完成持續(xù)集成中的測(cè)試金字塔。
2. Jenkins 上完成基于 KIF 的 UI 自動(dòng)化持續(xù)集成搭建
Jenkins 是一個(gè)開(kāi)源的持續(xù)集成工具,提供了一種易于使用的持續(xù)集成系統(tǒng),使開(kāi)發(fā)者從繁雜的集成中解脫出來(lái),專(zhuān)注于更為重要的業(yè)務(wù)邏輯實(shí)現(xiàn)上。 Jenkins 以 Job 為單位運(yùn)行項(xiàng)目,一個(gè) Job 的工作流程為:在指定的時(shí)機(jī),選擇合適的 salve 節(jié)點(diǎn),從版本管理系統(tǒng)上獲取對(duì)應(yīng)的源碼,使用命令行腳本或者 maven 或者 ant 進(jìn)行構(gòu)建,構(gòu)建后歸檔文件,處理報(bào)告,如果構(gòu)建失敗那么就通過(guò)郵件進(jìn)行反饋等。 Job 的觸發(fā)時(shí)機(jī)主要有3種選擇:
- “Build after other project are build”:表示在其他某個(gè)項(xiàng)目build后觸發(fā),比如我們可以在某個(gè)提測(cè)Job構(gòu)建之后,立即構(gòu)建我們的 UI 自動(dòng)化來(lái)驗(yàn)證這個(gè)提測(cè)的可行性;
- “Build periodically”:表示按時(shí)間觸發(fā),我們可以選擇這個(gè)讓 Job 做 Daily Build 來(lái)進(jìn)行持續(xù)構(gòu)建觀察;
- “Poll SCM”:表示允許用戶讓 Jenkins 定期查詢(xún)某一個(gè)項(xiàng)目的代碼庫(kù),如果有代碼變動(dòng)則觸發(fā)執(zhí)行任務(wù),這種觸發(fā)非常適合集成測(cè)試項(xiàng)目,以此驗(yàn)證代碼庫(kù)變動(dòng)是否能測(cè)試通過(guò)。
我們希望在代碼改動(dòng)發(fā)生的時(shí)候就做到盡早發(fā)現(xiàn)代碼改動(dòng)帶來(lái)的問(wèn)題,所以使用 “Poll SCM” 在當(dāng)代碼倉(cāng)庫(kù)有新的 pull request 的時(shí)候觸發(fā)相應(yīng) Job 完成構(gòu)建,Job 的執(zhí)行結(jié)果作為這個(gè) pull request 能否合入的衡量指標(biāo)之一;同時(shí)為支持客戶端支持 daily build ,Job 使用 “Build periodically” 在每天 daily build 打包前完成一次自動(dòng)構(gòu)建。 Job 需要支持命令行構(gòu)建才能實(shí)現(xiàn)持續(xù)集成,如上一部分提到,我們可以借助 xcodebuild/xctool 實(shí)現(xiàn)單命令行構(gòu)建。同時(shí)為了衡量 Job 的執(zhí)行結(jié)果,我們需要在 Job 執(zhí)行完成后生成相應(yīng)的測(cè)試報(bào)告和代碼覆蓋率報(bào)告,使用 xcodebuild/xctool 這樣的命令行工具,只需要配置相關(guān)的參數(shù)即可獲取相應(yīng)的 XML 測(cè)試報(bào)告文件。 Jenkins 中 JUnit Plugin 插件可以將 XML 形式的測(cè)試報(bào)告轉(zhuǎn)化成一種隨時(shí)間推移的測(cè)試結(jié)果圖表,向我們展示測(cè)試的結(jié)果和測(cè)試的穩(wěn)定性; Cobertura plugin 插件可以將 XML 形式的覆蓋率文件轉(zhuǎn)化成一種隨時(shí)間推移的代碼覆蓋率圖表。如下圖是 Job 中測(cè)試報(bào)告的代碼覆蓋率和測(cè)試結(jié)果的示例,通過(guò)下面的圖表,我們可以清晰地看到測(cè)試是否通過(guò),檢查代碼的測(cè)試覆蓋范圍,并對(duì)比歷史的測(cè)試結(jié)果和代碼覆蓋率來(lái)推斷和定位問(wèn)題。
3. KIF 自動(dòng)化測(cè)試在 Jenkins 持續(xù)集成過(guò)程中遇到的問(wèn)題
(1)設(shè)備重置
我們的測(cè)試用例覆蓋了第一次安裝啟動(dòng)的操作。在初期,這個(gè)用例經(jīng)常失敗。經(jīng)過(guò)排查發(fā)現(xiàn),持續(xù)集成系統(tǒng)中的模擬器設(shè)備重置操作并沒(méi)有覆蓋所有的設(shè)備,UI 測(cè)試 Job 運(yùn)行時(shí),Job 選擇的模擬器設(shè)備上可能遺留了其他 Job 構(gòu)建的相同的 app 產(chǎn)物,導(dǎo)致我們的 Job 構(gòu)建產(chǎn)物并不是第一次安裝啟動(dòng)。所以在腳本中我們遍歷所有模擬器設(shè)備,將其進(jìn)行重置。
(2)鍵盤(pán)敲擊延遲
我們的測(cè)試用例在輸入框輸入文字時(shí),經(jīng)常出現(xiàn)輸入不全而導(dǎo)致失敗的問(wèn)題。比如在輸入框中輸入 ‘beijing’ ,失敗后提示:Failed to get text in field; instead, it was ‘beiji’ 。經(jīng)過(guò)排查,發(fā)現(xiàn)持續(xù)集成系統(tǒng)中的機(jī)器性能有高有低,在低性能機(jī)器中更容易發(fā)生此問(wèn)題,再研究 KIF 框架源碼發(fā)現(xiàn),KIF 默認(rèn)設(shè)置的鍵盤(pán)敲擊時(shí)延為一個(gè)常數(shù),對(duì)于低性能機(jī)器來(lái)說(shuō)這個(gè)敲擊時(shí)延較短,容易漏掉輸入,所以我們?cè)?KIFTypist.m 源碼文件中適當(dāng)增加 (NSTimeInterval) keystrokeDelay 的時(shí)長(zhǎng)來(lái)避免輸入不全的問(wèn)題。
(3)多個(gè)系統(tǒng)彈窗確認(rèn)
前面我們提到過(guò),KIF 支持對(duì)系統(tǒng)彈窗的處理,即接口 acknowledgeSystemAlert ,它能幫我們確認(rèn)一個(gè)系統(tǒng)彈窗。但是我們的應(yīng)用程序在啟動(dòng)時(shí)系統(tǒng)彈窗并不止一個(gè),并且在不同設(shè)備上,因系統(tǒng)設(shè)置不同,系統(tǒng)彈窗的個(gè)數(shù)是不確定的。所以,直接使用 acknowledgeSystemAlert 并不能幫我們解決問(wèn)題。因?yàn)?KIF 的開(kāi)源性,我們?cè)?KIF 框架源碼 acknowledgeSystemAlert 函數(shù)中做了一次 while 循環(huán)處理,處理了出現(xiàn)的任意多個(gè)系統(tǒng)彈窗的情況,從而解決了問(wèn)題。
參考文獻(xiàn)
總結(jié)
以上是生活随笔為你收集整理的基于 KIF 的 iOS UI 自动化测试和持续集成的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: @FeignClient中的@Reque
- 下一篇: 震惊!丧心病狂的夕小瑶推出新一轮写作计划