自動化UI Test

App版本迭代速度非常快,每次發版本前都需要回歸一些核心測試用例,人工回歸枯燥且重復勞動。自動化UI Test雖然不能完全代替人工,但能幫助分擔大部分測例。能讓機器干的就不要讓人來干了,從自動化、UI Test兩個方面來講下怎么實現自動化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能幫助我們去驗證一些UI元素的屬性和狀態。

怎么做

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這類控件,那怎么獲取到這類元素呢?一種方式是通過下標,但這種方式非常不穩定,很容易出現問題。

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()

但是這樣這種方式對業務的侵入太嚴重了,在沒有一個合適方案的情況下,可以考慮下面這種在殼工程中通過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只識別文字,但是有一個特殊的地方,如果圖片在按鈕上,那么這個按鈕所在區域也會被識別,默認identifier就是圖片的名稱。

借助Accessibility,可以突破上面描述的限制,你可以為某一個控件的accessibilityIdentifier屬性賦值,這樣,UI Test就能通過這個值獲取到相應的控件。Accessibility本身是為有障礙人士設計的,當你的手摸到那個控件時,iPhone會通過語音告訴你這是什么東西。

可惜的是目前還沒特別好的方法讓Accessibility和業務本身解耦。

忽略后端因素

后端因素主要指網絡和數據,接口返回時機不可控,依賴于網絡和服務器,但Test Case需要等到接口返回,并且App布局完成后才能開始,因此,大部分測例開始前,可以加入類似下面這樣的代碼,來判斷App是否渲染完成。

expectation(for: NSPredicate(format:"count > 0"), evaluatedWith: app.tables, handler: nil)        
waitForExpectations(timeout: 3, handler: nil)

另一個便是數據了,接口返回的數據變化不可控,但Test Case卻是固定的。解決這個問題我想到了下面兩個方案:

  • Mock數據
  • Test Case開始前,通過調用后端接口,造出相同的數據

Mock數據

mock.png

上圖中,點擊每個按鈕后,都會hook獲取數據的方法,將url替換成對應的mock數據url,這些工作都在殼工程中完成,不會對業務代碼產生侵入性。

造出相同數據

// 在setUp中設置launchArguments
let app = XCUIApplication()
app.launchArguments = ["cart_testcase_start"]


// 在application:didFinishLaunchingWithOptions:中監測啟動
NSArray *args = [NSProcessInfo processInfo].arguments;
for (int i = 1; i < args.count; ++i) {
    // 檢測到購物車相關測例即將開始,開始創造前置條件
    if ([args[i] isEqualToString:@"cart_testcase_start"]) {
        // 加入購物車
        ...
    }
}

小結

上述方案已經能滿足大部分Test Case的需求了,但局限性依舊存在,比如UI Test本身無法識別圖片,這就意味著無法繞過圖形驗證碼,另外就是短信驗證碼這類(Android貌似可以做到)。其他測例,理論上只要App內能完成的,Test Case就能覆蓋到,但這就涉及到成本問題了,在殼工程內寫肯定比在主工程中簡單。

一些優化

  • 類似內存泄露等通用檢測,可以統一處理,不必每個測例都寫一遍
  • 測例開始后,每隔一段時間,XCTest框架會去掃描一遍App,動畫的存在有時候會擾亂你獲取界面元素,因此最好關閉動畫
func customSetUp() -> XCUIApplication {
        super.setUp()
        continueAfterFailure = true
        let app = XCUIApplication()
        // 在AppDelegate啟動方法中監測animationsEnable,然后設置下關閉動畫
        app.launchEnvironment = ["animationsEnable": "NO"]
        memoryLeak()
        return app
}

// 這里在工程中用了MLeakFinder,所以只要監測彈窗即可
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 {
                
                // 拼接造成內存泄露的原因
                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:觸發自動化流程,git commit -m "班車自動化UI Test" & git push

step2:觸發流程后,會在gitlab相應項目中生成一個build,等待build結束

ui-test-build.jpg

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

ui-test-build-info.jpg

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

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怎么和業務解耦之類的。

殼工程:主工程包含了所有需要的Pods,殼工程值能運行Pods的環境,可以包含一個或多個Pods

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,818評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,185評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,656評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,647評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,446評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,951評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,041評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,189評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,718評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,800評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,419評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,420評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,755評論 2 371

推薦閱讀更多精彩內容