自動化UI Test

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這個東西的作用放到下面講。

難點

獲取指定元素

app_ui.jpg

獲取按鈕

// 通過按鈕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ù)

mock.png

上圖中,點擊每個按鈕后丰榴,都會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結束

ui-test-build.jpg

step3:點擊build查看詳情岩调,通過下圖可以看到這次build失敗巷燥,原因是Detail的5個測例沒有通過

ui-test-build-info.jpg

step4:在gitlab中顯示的日志量比較少,是因為gitlab對日志量做了限制号枕,所以在gitlab中的日志都是經(jīng)過篩選的關鍵信息缰揪,具體錯誤原因通過查看服務器日志,下圖日志說明了因為內(nèi)存泄露導致了對應測例失敗

ui-test-log.jpg

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

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市近零,隨后出現(xiàn)的幾起案子诺核,更是在濱河造成了極大的恐慌抄肖,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件窖杀,死亡現(xiàn)場離奇詭異漓摩,居然都是意外死亡,警方通過查閱死者的電腦和手機入客,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門管毙,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人桌硫,你說我怎么就攤上這事夭咬。” “怎么了铆隘?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵卓舵,是天一觀的道長。 經(jīng)常有香客問我膀钠,道長掏湾,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任肿嘲,我火速辦了婚禮融击,結果婚禮上,老公的妹妹穿的比我還像新娘睦刃。我一直安慰自己砚嘴,他們只是感情好,可當我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布涩拙。 她就那樣靜靜地躺著际长,像睡著了一般。 火紅的嫁衣襯著肌膚如雪兴泥。 梳的紋絲不亂的頭發(fā)上工育,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天,我揣著相機與錄音搓彻,去河邊找鬼如绸。 笑死,一個胖子當著我的面吹牛旭贬,可吹牛的內(nèi)容都是我干的怔接。 我是一名探鬼主播,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼稀轨,長吁一口氣:“原來是場噩夢啊……” “哼扼脐!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起奋刽,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤瓦侮,失蹤者是張志新(化名)和其女友劉穎艰赞,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體肚吏,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡方妖,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了罚攀。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片党觅。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖坞生,靈堂內(nèi)的尸體忽然破棺而出仔役,到底是詐尸還是另有隱情掷伙,我是刑警寧澤是己,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站任柜,受9級特大地震影響卒废,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜宙地,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一摔认、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧宅粥,春花似錦参袱、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至企垦,卻和暖如春环壤,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背钞诡。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工郑现, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人荧降。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓接箫,卻偏偏與公主長得像,于是被迫代替她去往敵國和親朵诫。 傳聞我的和親對象是個殘疾皇子辛友,可洞房花燭夜當晚...
    茶點故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內(nèi)容