你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> 自動化UI Test

自動化UI Test

編輯:IOS開發基礎

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

難點

獲取指定元素

1199576-2d5c362e93d24122.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數據

1199576-41f570d8ce8afc6d.png.jpeg

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

1199576-35a6ad4186e6cd32.jpg

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

1199576-03b58990bcb47640.jpg

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

1199576-a911a2805050c05b.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




  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved