12/08/2018, 15:58

Xcode UI Testing

Trong bài viết này mình sẽ giới thiệu về Xcode UI Testing và một số lỗi thường gặp khi chạy UI automation. Xcode có thể generate code khi bạn tương tác với ứng dụng của mình. Chúng ta hãy bắt đầu bằng việc tạo một UI Testing target. Nút record lại bị vô hiệu hóa lúc đầu, bởi vì bạn cần phải chạy ...

Trong bài viết này mình sẽ giới thiệu về Xcode UI Testing và một số lỗi thường gặp khi chạy UI automation.

Xcode có thể generate code khi bạn tương tác với ứng dụng của mình. Chúng ta hãy bắt đầu bằng việc tạo một UI Testing target. Nút record lại bị vô hiệu hóa lúc đầu, bởi vì bạn cần phải chạy test thành công một lần. Để làm được điều đó thì ta chọn Product -> Test, đồng thời đảm bảo các test case của bạn được liệt kê trong target scheme của ứng dụng của bạn trước khi run test. Tuy nhiên rất có thể bạn không muốn sử dụng generated code, trong trường hợp đó bạn phải làm gì? Tiếp theo chúng ta sẽ cùng tìm hiểu sâu hơn về vấn đề này.

Code được generate từ Xcode sẽ truy cập đến các element bằng cách sử dụng Accessibility Identifier, ví dụ như:

app.buttons["back"].tap()

Lưu ý rằng "back" là một identifier được compare với "Back" trong tiếng anh. Bạn KHÔNG nên sử dụng accessibility name vì có thể được bản địa hoá, vì vậy chạy bằng các ngôn ngữ khác nhau sẽ không hoạt động như mong đợi. Bạn nên luôn luôn sử dụng accessibility identifier. Một số thành phần như UIBarButtonItem không có accessibility identifier field trong identifier inspector. Bạn vẫn có thể thêm nó vào user defined runtime attributes với kiểu String.

Trước khi chúng ta tìm hiểu cách để chọn các elements ta sẽ tìm hiểu việc print UI Hierarchy, nó giúp cho việc chọn các elements dễ dàng hơn. Bạn có thể viết lệnh print với element debugDescription. Hoặc bạn có thể set breakpoint sau đó print với po anElement.

Other 0x7ff0014a3bc0: traits: 8589934592, {{0.0, 0.0}, {414.0, 736.0}}
   Other 0x7ff00149a490: traits: 8589934592, {{0.0, 0.0}, {414.0, 736.0}}
     NavigationBar 0x7ff00146b8a0: traits: 35192962023424, {{0.0, 20.0}, {414.0, 44.0}}, identifier: 'home'
       Image 0x7ff00174bde0: traits: 8589934596, {{0.0, 0.0}, {414.0, 64.0}}
         Image 0x7ff0014984f0: traits: 8589934596, {{0.0, 64.0}, {414.0, 0.3}}
       Button 0x7ff0014a4240: traits: 8724152321, {{12.0, 31.7}, {21.0, 21.0}}, identifier: 'back'

Bạn có thể dễ dàng nhìn thấy views hierarchy, type, and identifier. Điều này rất tiện dụng trong việc xác định Accessibility Identifier.

# Để select row 0
app.tables.cells.elementBoundByIndex(0).tap()

Sử dụng childrenMatchingType , bạn có thể get element là con của một element khác. Ở vế sau, nó sẽ lấy tất cả các con thuộc kiểu Image và ở vị trí 2

anElement.childrenMatchingType(.Image).elementBoundByIndex(2)

Bạn có thể viết các câu lệnh logic và làm điều gì đó với một element chỉ khi element đó tồn tại. Để kiểm tra một element tồn tại, đơn giản chỉ cần sử dụng thuộc tính exists của một XCUIElement

if app.buttons["back"].exists {
  // Do Something
}

Đôi khi phải mất một thời gian để một element xuất hiện. Sau đây là ta sẽ đợi 5 giây để nút "back" xuất hiện.

let backButton = app.buttons["back"]
let exists = NSPredicate(format: "exists == true")
expectationForPredicate(exists, evaluatedWithObject: backButton, handler: nil)
waitForExpectationsWithTimeout(5, handler: nil)

Đối với các lỗi lạ , một số thiết bị nhất định có thể không thể tap vào element, ngay cả khi nó tappable. Giải pháp để giải quyết tình trạng này là sử dụng force tap. Khi một phần tử không thể tap được, ta sẽ get toạ độ của nó và make a tap.

extension XCUIElement {
    func forceTap() {
        if self.hittable {
            self.tap()
        } else {
            let coordinate: XCUICoordinate = self.coordinateWithNormalizedOffset(CGVectorMake(0.0, 0.0))
            coordinate.tap()
        }  
    }
}

Ví dụ như nếu gặp lỗi khi sửu dụng tap(), bạn có thể thay đổi thành forceTap():

app.buttons["back"].forceTap()

Chúng ta đã sử dụng XCUICoordinate ở trên. Nó là một cách nâng cao để tương tác với các elements thông qua toạ độ của chúng. XCUIElement.coordinateWithNormalizedOffset tạo một toạ độ mới bằng cách nhân với nomalized offset. XCUICoordinate.coordinateWithOffset tạo ra một tọa độ mới, bằng cách thêm offset.

Bạn có thể sleep app bằng cách sử dụng sleep(thời gian tính bằng giây).

Rất có thể UI cho iPhone và iPad là khác nhau. Để xử lý, bạn nên có 2 trường hợp test:

func testSnapshotPhone() {
    guard UIDevice.currentDevice().userInterfaceIdiom == .Phone else { return }
    // Capture screenshots for iPhone
}

func testSnapshotPad() {
    guard UIDevice.currentDevice().userInterfaceIdiom == .Pad else { return }
    // Capture screenshots for iPad
}

Bạn có thể khởi chạy ứng dụng bằng các behaviours đặc biệt, bằng cách truyền các launch arguments hoặc các environnment variables. Đây là 2 cách riêng biệt, nhưng chúng rất giống nhau. Sự khác biệt chính là launch argument là một chuỗi, còn environnment variable là một key-value. Nhưng khi sử dụng launch argument, bạn vẫn có thể sử dụng nó như là một key-value, ví dụ như -SPECIAL_FEATURE YES Đây là cách bạn khởi chạy ứng dụng của mình với custom argument trong setUp

override func setUp() {
    super.setUp()
    app = XCUIApplication()
    app.launchArguments += ["-SPECIAL_FEATURE", "YES"]
    app.launch()
}

Sau đó trong ứng dụng của bạn, bạn có thể biết launch argument với NSUserDefaults:

NSUserDefaults.standardUserDefaults().boolForKey("SPECIAL_FEATURE")

Bạn cũng có thể sử dụng NSProcessInfo.processInfo().arguments. Đối với các environment variables, bạn có thể sử dụng app.launchEnvironmentNSProcessInfo.processInfo().environment tương ứng. Lưu ý rằng XCUIApplication không phải là singleton! Vì vậy, đừng cố gắng để có nhiều XCUIApplication () trong test case của bạn. Chỉ Có 1 biến đối tượng var app: XCUIApplication! trong test case của bạn được sử dụng trong tất cả các method. Hy vọng rằng bài viết của mình sẽ giúp các bạn hiểu hơn về Xcode UI Testing.

0