App版本迭代速度非常快儒恋,每次發(fā)版本前都需要回歸一些核心測試用例善绎,人工回歸枯燥且重復勞動。自動化UI Test雖然不能完全代替人工诫尽,但能幫助分擔大部分測例禀酱。能讓機器干的就不要讓人來干了,從自動化牧嫉、UI Test兩個方面來講下怎么實現(xiàn)自動化UI Test剂跟。
UI Test
有什么用
UI testing gives you the ability to find and interact with the UI of your app in order to validate the properties and state of the UI elements.
官方文檔說的很簡潔明了,UI Test能幫助我們?nèi)ヲ炞C一些UI元素的屬性和狀態(tài)酣藻。
怎么做
UI tests rests upon two core technologies: the XCTest framework and Accessibility.
- XCTest provides the framework for UI testing capabilities, integrated with Xcode. Creating and using UI testing expands upon what you know about using XCTest and creating unit tests. You create a UI test target, and you create UI test classes and UI test methods as a part of your project. You use XCTest assertions to validate that expected outcomes are true. You also get continuous integration via Xcode Server and xcodebuild. XCTest is fully compatible with both Objective-C and Swift.
- Accessibility is the core technology that allows disabled users the same rich experience for iOS and OS X that other users receive. It includes a rich set of semantic data about the UI that users can use can use to guide them through using your app. Accessibility is integrated with both UIKit and AppKit and has APIs that allow you to fine-tune behaviors and what is exposed for external use. UI testing uses that data to perform its functions.
UI Test主要借助XCTest和Accessibility兩個東西曹洽,其中XCTest框架幫我做了大部分事情,我們只要往testExample
這個方法里填空就能將整個流程跑起來辽剧,每個以test
開頭的方法都會被當成一個測例送淆。
class EBTest: XCTestCase {
override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
// In UI tests it is usually best to stop immediately when a failure occurs.
continueAfterFailure = false
// UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method.
XCUIApplication().launch()
// In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
}
func testExample() {
// Use recording to get started writing UI tests.
// Use XCTAssert and related functions to verify your tests produce the correct results.
}
}
Accessibility這個東西的作用放到下面講。
難點
獲取指定元素
獲取按鈕
// 通過按鈕title獲取
app.buttons["立即購買"]
// 通過圖片資源名稱獲取
// btn navigationbar back可以通過Accessibility Inspector這個工具查看
app.buttons["btn navigationbar back"]
獲取文本
// 直接獲取
app.staticTexts["豪華午餐"]
// 通過NSPredicate匹配獲取
let predicate = NSPredicate(format:"label BEGINSWITH %@", "距離雙12")
app.staticTexts.element(matching:predicate)
上面兩種方式只能獲取到文本和按鈕怕轿,但是無法獲取UIImageView坊夫、UITableViewCell這類控件,那怎么獲取到這類元素呢撤卢?一種方式是通過下標环凿,但這種方式非常不穩(wěn)定,很容易出現(xiàn)問題放吩。
app.tables.element(boundBy: 0).cells.element(boundBy: 0)
另一種方式就是通過Accessibility智听,我們可以為一個元素設置accessibilityIdentifier
屬性,這樣就能獲取到這個元素了渡紫。
// 生成button時設置accessibilityIdentifier
- (UIButton *)buildButtonWithTitle:(NSString *)title identifier:(NSString *)identifier handler:(void (^)())handler {
UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
[button setTitle:title forState:UIControlStateNormal];
button.frame = [self buildFrame];
button.accessibilityIdentifier = identifier;
[self.view addSubview:button];
[button bk_addEventHandler:^(id sender) {
handler();
} forControlEvents:UIControlEventTouchUpInside];
return button;
}
// 通過設置的accessibilityIdentifier來獲取這個按鈕
app.buttons.matching(identifier: "EnterGoodsDetailNormal").element.tap()
但是這樣這種方式對業(yè)務的侵入太嚴重了到推,在沒有一個合適方案的情況下,可以考慮下面這種在殼工程中通過hook來設置accessibilityIdentifier惕澎。
- (void)hook {
static NSArray *hookArray = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
hookArray = @[ @"SomeClass", @"AnotherClass" ];
SEL originalSelector = @selector(accessibilityIdentifier);
SEL swizzledSelector = @selector(eb_accessibilityIdentifier);
[hookArray enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
[EBHookUtil eb_swizzleInstanceMethod:NSClassFromString(obj) swClass:[EBAppDelegate class] oriSel:originalSelector swSel:swizzledSelector];
}];
});
}
- (NSString *)eb_accessibilityIdentifier {
return NSStringFromClass([self class]);
}
小結
我們看到的App界面由文字和圖片組成莉测,而UI Test只識別文字,但是有一個特殊的地方唧喉,如果圖片在按鈕上捣卤,那么這個按鈕所在區(qū)域也會被識別忍抽,默認identifier就是圖片的名稱。
借助Accessibility董朝,可以突破上面描述的限制鸠项,你可以為某一個控件的accessibilityIdentifier屬性賦值,這樣子姜,UI Test就能通過這個值獲取到相應的控件祟绊。Accessibility本身是為有障礙人士設計的,當你的手摸到那個控件時哥捕,iPhone會通過語音告訴你這是什么東西牧抽。
可惜的是目前還沒特別好的方法讓Accessibility和業(yè)務本身解耦。
忽略后端因素
后端因素主要指網(wǎng)絡和數(shù)據(jù)遥赚,接口返回時機不可控阎姥,依賴于網(wǎng)絡和服務器,但Test Case需要等到接口返回鸽捻,并且App布局完成后才能開始呼巴,因此,大部分測例開始前御蒲,可以加入類似下面這樣的代碼衣赶,來判斷App是否渲染完成。
expectation(for: NSPredicate(format:"count > 0"), evaluatedWith: app.tables, handler: nil)
waitForExpectations(timeout: 3, handler: nil)
另一個便是數(shù)據(jù)了厚满,接口返回的數(shù)據(jù)變化不可控府瞄,但Test Case卻是固定的。解決這個問題我想到了下面兩個方案:
- Mock數(shù)據(jù)
- Test Case開始前碘箍,通過調(diào)用后端接口遵馆,造出相同的數(shù)據(jù)
Mock數(shù)據(jù)
上圖中,點擊每個按鈕后丰榴,都會hook獲取數(shù)據(jù)的方法货邓,將url替換成對應的mock數(shù)據(jù)url,這些工作都在殼工程中完成四濒,不會對業(yè)務代碼產(chǎn)生侵入性换况。
造出相同數(shù)據(jù)
// 在setUp中設置launchArguments
let app = XCUIApplication()
app.launchArguments = ["cart_testcase_start"]
// 在application:didFinishLaunchingWithOptions:中監(jiān)測啟動
NSArray *args = [NSProcessInfo processInfo].arguments;
for (int i = 1; i < args.count; ++i) {
// 檢測到購物車相關測例即將開始,開始創(chuàng)造前置條件
if ([args[i] isEqualToString:@"cart_testcase_start"]) {
// 加入購物車
...
}
}
小結
上述方案已經(jīng)能滿足大部分Test Case的需求了盗蟆,但局限性依舊存在戈二,比如UI Test本身無法識別圖片,這就意味著無法繞過圖形驗證碼喳资,另外就是短信驗證碼這類(Android貌似可以做到)觉吭。其他測例,理論上只要App內(nèi)能完成的仆邓,Test Case就能覆蓋到鲜滩,但這就涉及到成本問題了伴鳖,在殼工程內(nèi)寫肯定比在主工程中簡單。
一些優(yōu)化
- 類似內(nèi)存泄露等通用檢測绒北,可以統(tǒng)一處理黎侈,不必每個測例都寫一遍
- 測例開始后察署,每隔一段時間闷游,XCTest框架會去掃描一遍App,動畫的存在有時候會擾亂你獲取界面元素贴汪,因此最好關閉動畫
func customSetUp() -> XCUIApplication {
super.setUp()
continueAfterFailure = true
let app = XCUIApplication()
// 在AppDelegate啟動方法中監(jiān)測animationsEnable脐往,然后設置下關閉動畫
app.launchEnvironment = ["animationsEnable": "NO"]
memoryLeak()
return app
}
// 這里在工程中用了MLeakFinder,所以只要監(jiān)測彈窗即可
func memoryLeak() {
addUIInterruptionMonitor(withDescription: "Memory Leak, Big Brother") { (alert) -> Bool in
if alert.staticTexts["Memory Leak"].exists ||
alert.staticTexts["Retain Cycle"].exists ||
alert.staticTexts["Object Deallocated"].exists {
// 拼接造成內(nèi)存泄露的原因
var msg = ""
let title = alert.staticTexts.element(boundBy: 0)
if title.exists {
msg += "標題:" + title.label
}
let reason = alert.staticTexts.element(boundBy: 1)
if reason.exists {
msg += " 原因:" + reason.label
}
XCTFail("Memory Leak, Big Brother " + msg)
alert.buttons["OK"].tap()
return true
}
return false
}
}
自動化
在自動化方面扳埂,主要借助Gitlab CI业簿,具體怎么配置Gitlab CI就不在這里展開了,參考官方文檔
先來看看最后的流程:
step1:觸發(fā)自動化流程阳懂,git commit -m "班車自動化UI Test" & git push
step2:觸發(fā)流程后梅尤,會在gitlab相應項目中生成一個build,等待build結束
step3:點擊build查看詳情岩调,通過下圖可以看到這次build失敗巷燥,原因是Detail的5個測例沒有通過
step4:在gitlab中顯示的日志量比較少,是因為gitlab對日志量做了限制号枕,所以在gitlab中的日志都是經(jīng)過篩選的關鍵信息缰揪,具體錯誤原因通過查看服務器日志,下圖日志說明了因為內(nèi)存泄露導致了對應測例失敗
step5:build失敗葱淳,郵件通知钝腺,交由相應組件負責人處理
再來看看.gitlab-ci.yml文件,這個文件是Gitlab CI的配置文件赞厕,CI要做什么都可以在這個文件里面描述
stages:
- test
before_script:
- cd Example
- pod update
ui_test:
stage: test
script:
# 這里先build一下艳狐,防止log日志過多,防止gitlab ci build log exceeded limit of 4194304 bytes.
- xcodebuild -workspace IntegrationTesting.xcworkspace -scheme IntegrationTesting-Example -destination 'platform=iOS Simulator,name=iPhone 7,OS=10.1' build >/dev/null 2>&1
- xcodebuild -workspace IntegrationTesting.xcworkspace -scheme IntegrationTesting-Example -destination 'platform=iOS Simulator,name=iPhone 7,OS=10.1' test | tee Log/`date +%Y%m%d_%H%M%S`.log | grep -A 5 'error:'
結束
這套方案應該算是比較初級的皿桑,自動化平臺也建議大家用Jenkins僵驰,Gitlab CI有點弱,另外唁毒,大家有什么好點子可以多多交流蒜茴,像Accessibility怎么和業(yè)務解耦之類的。
注
殼工程:主工程包含了所有需要的Pods浆西,殼工程值能運行Pods的環(huán)境粉私,可以包含一個或多個Pods