XCode 7 正式版已经正式发布,大家可以通过 App Store 下载。XCodeGhost 事件最近闹得沸沸扬扬,第三方下载一定要慎重。
在 XCode 7 beta 的时候,我们就第一时间介绍过 UI Testing。
如今,我觉得有必要和大家再来看看这个工具,它可能依然不那么完美,可能都谈不上好用,但是这是一个明显的风向标,而且有很多小道消息也指出,XCUITesting 会慢慢取代 UIAUtomation。
本文将结合 joemasilotti 的两篇文章和 WWDC 的视频 ,有兴趣的同学一定请直接阅读和观看。
P.S 本文 UITesting 方面经过验证,其他的来自互联网,请自行实践。
事实上 UI Testing 并没有啥新意,它提供了类似于 UIAutomation 的功能,比如:
这些在 UIAutomation 都有体现。为什么有了 UIAutomation 还推一个 UI Testing 的原因,个人的猜测是:
XCTest
UI Testing 是基于 XCTest 测试框架的。XCTest 作为 OCUnit 的替代者,目前是 iOS 单元测试框架不二之选,很多其他测试框架也基于 XCTest 封装。XCTest 有如下特点:
那 UI Testing 在 XCTest 的基础上实际上是扩展了几个类,协议,如图:
所以本质上 UI Testing 还是 XCTest,所以写用例的时候,还是需要遵从 XCTest 的规则。
Accessibility
Accessibility 是 Apple 很早之前构建的一个框架,它能帮助一些行动不便的用户来更好地使用你的应用。它为你的 UI 提供了丰富的语义数据,这能让不同的 Accessibility 功能给行动不便的用户展现你的应用。有很多功能都是现成的,直接就能在你的应用中使用,但是你可以(也应该)使用 Accessibility 的 API 来改进 Accessibility 关于 UI 的数据。在很多场景下这都是必需的,比如对一些自定义的控件,Accessibility 就不清楚你的 API 要做什么。
—— 实战 iOS9【第二天】UI Testing
我曾经在讲解 Appium 上的 iOS 时候,提到了 Accessibility,因为 Appium 底层的 UIAutomation 是需要依靠 Accessibility 来定位元素。所以在 iOS 中做 UI 自动化测试,就仿佛盲人使用应用,如果没有可见性,什么都做不成。
同样,XCUITesting 也走了这条路。UI Testing 可以通过你的应用提供的 Accessibility 功能来与你的应用连接,这样就解决了设备大小不一的问题。如果你重新调整了 UI 中的某些元素,你也不用重写整套测试。当然实现 Accessibility 的本质不是为了使用 UI Testing,而是为了能帮助行动不便的用户更好地使用你的应用。
我是跟着 WWDC 视频走了一遍,所以希望大家也可以做一遍。我们需要准备些物料:
这四个步骤最有问题的可能是第三步,虽然代码中的 README 提供了详细的方法,但是我还是想提示下同学 Bundle ID 一定要改成自己要的,而且最好把项目中所有的 Bundle ID 或者 BUNDLE_PREFIX 换成自己的,并且要统一。
Setup
当你在 Xcode 7 中创建新工程时,可以选择是否要包含 UI 测试。这会为你设置一个占位的 UI Test target,并且配置好了所需的内容。如图:
创建一个 Target,选择 UI Testing:
选择测试的 Target:
创建好之后,会生成 ListerUITests 目录,底下有 ListerUITests.swift 文件:
//
// ListerUITests.swift
// ListerUITests
//
// Created by lihua zhang on 15/9/20.
// Copyright © 2015年 Apple Inc. All rights reserved.
//
import XCTest
class ListerUITests: 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.
}
}
我们先只需关心 testExample 这个方法。我们先用 UI Recording 来录制并生成代码:
从 gif 里面可以看出来,其实我们的录制过程并没有像 WWDC 视频中那么顺利,其中因为模拟器输入法的问题,cookie 变成了 can -_-!(打开模拟器的 Software keyboard 就可以了),我猜想苹果肯定演练了很多次。那我们生成的代码,大致是这样的:
let app = XCUIApplication()
let tablesQuery = app.tables
tablesQuery.staticTexts["Groceries"].tap()
tablesQuery.textFields["Add Item"].tap()
tablesQuery.childrenMatchingType(.Cell).elementBoundByIndex(0).childrenMatchingType(.TextField).element.typeText("cookie")
app.typeText("\r")
tablesQuery.childrenMatchingType(.Cell).elementBoundByIndex(1).childrenMatchingType(.TextField).element.tap()
let groceriesNavigationBarsQuery = app.navigationBars.matchingIdentifier("Groceries")
groceriesNavigationBarsQuery.buttons["Edit"].tap()
tablesQuery.childrenMatchingType(.Cell).elementBoundByIndex(7).buttons["Delete "].tap()
tablesQuery.buttons["Delete"].tap()
groceriesNavigationBarsQuery.buttons["Done"].tap()
其实和 UIAutomation 的录制一样惨不忍睹。那我们还是需要修改下:
func testExample() {
let app = XCUIApplication()
let tablesQuery = app.tables
tablesQuery.staticTexts["Groceries"].tap()
tablesQuery.textFields["Add Item"].tap()
tablesQuery.textFields["Add Item"].typeText("Cookie")
app.typeText("\r")
let cells = tablesQuery.childrenMatchingType(.Cell)
let cellQuery = cells.containingType(.TextField, identifier:"Cookie")
let textField = cellQuery.childrenMatchingType(.TextField).element
textField.tap()
app.navigationBars.matchingIdentifier("Groceries").buttons["Edit"].tap()
cellQuery.buttons["Delete "].tap()
tablesQuery.buttons["Delete"].tap()
app.navigationBars.matchingIdentifier("Groceries").buttons["Done"].tap()
}
小贴士:使用 Accessibility Inspector 来查看元素
某些时候,在录制时,当你点击了一个元素,你可能会注意到生成的代码看上去不太对。这通常是因为你正在交互的元素对 Accessibility 不可见。你可以使用 Xcode 的 Accessibility Inspector 来检查是不是这种情况。
当打开 Accessibility Inspector 之后 ,当你把鼠标悬停在模拟器中的元素上,就能看到鼠标指针下面元素的详细信息。这个时候,如果你按下 CMD+F7,就会锁定该元素,你可以点击每个属性进行查看。
总的来说,这个 Inspector 能帮助你定位一些元素,但是有时候会不稳定,需要重启模拟器。
从 Demo 的过程可以看出,录制的确可以帮助生成代码,对于生产力有一定的帮助,但是和大多数 UI 自动化框架一样,录制是不被推荐的,理由如下:
我们需要用到的 UI 方面的 API 基本是围绕这三个类进行的,断言之类依然使用 XCTest 的 API。
XCUIApplication
通过 XCUIApplication 来启动应用,该类是被测应用的一个代理。我们可以这样维护被测应用:
class UITests: XCTestCase {
let app = XCUIApplication()
override func setUp() {
super.setUp()
continueAfterFailure = false
app.launch()
}
}
UI Testing 每次启动都会启动一个新的进程,重新启动应用。感觉和 Junit 是一样的。
XCUIElement
XCUIElement 封装了应用里的 button,textField,table,cell 等等信息。
Elements are objects encapsulating the information needed to dynamically locate a user interface element in an application. Elements are described in terms of queries. When an event API is called, the element will be resolved. If zero or multiple matches are found, an error will be raised.
我们可以通过元素的 type 和 Accessibility 或者两者结合起来定位元素,如果没有找到元素,或者多个元素返回都会报错,也就是无论用啥来定位,要进行后续操作的元素,必须有唯一性(比如,返回的是一个集合,你不能对集合直接操作,但是你可以对集合里面的单个元素操作)。
元素的层级和 UIAutomation 没有差别,层级逐阶而下。
我们可以用 debugDescription 来返回信息,类似 UIAutomation 的 logElementTree。
(lldb) po cellQuery.debugDescription
t = 113.76s Snapshot accessibility hierarchy for com.testerhome.Lister
t = 114.03s Find: Descendants matching type Table
t = 114.03s Find: Children matching type Cell
t = 114.04s Find: Elements containing elements matching type TextField with identifier 'Cookie'
"Find: Target Application 0x7fa7515615c0\n Output: {\n Application 0x7fa751621db0: {{0.0, 0.0}, {320.0, 568.0}}, label: \'Lister\'\n }\n ↪︎Find: Descendants matching type Table\n Output: {\n Table 0x7fa7515eb050: traits: 35192962023424, {{0.0, 0.0}, {320.0, 568.0}}\n }\n ↪︎Find: Children matching type Cell\n Output: {\n Cell 0x7fa7515eb830: traits: 8589934592, {{0.0, 64.0}, {320.0, 44.0}}\n Cell 0x7fa7515ec720: traits: 8589934592, {{0.0, 108.0}, {320.0, 44.0}}\n Cell 0x7fa7515ed610: traits: 8589934592, {{0.0, 152.0}, {320.0, 44.0}}\n Cell 0x7fa7515ee520: traits: 8589934592, {{0.0, 196.0}, {320.0, 44.0}}\n Cell 0x7fa7515ef410: traits: 8589934592, {{0.0, 240.0}, {320.0, 44.0}}\n }\n ↪︎Find: Elements containing elements matching type TextField with identifier \'Cookie\'\n Output: {\n Cell 0x7fa7515ec720: traits: 8589934592, {{0.0, 108.0}, {320.0, 44.0}}\n }\n"
XCUIElement 提供了很多交互方法,不过根据平台还是有些区别,比如:
具体可以参考: http://masilotti.com/xctest-documentation/Classes/XCUIElement.html
XCUIElementQuery
Query 估计是打交道最多的,大多数时候我们通过层级关系或者特定信息来定位元素。
特定信息:
比如:
let tablesQuery = app.tables // 定位到 App 中 Table View
tablesQuery.staticTexts["Groceries"].tap() // 通过 Accessibility Value “Groceries” 来定位
tablesQuery.textFields["Add Item"].tap() //
app.navigationBars.matchingIdentifier("Groceries").buttons["Edit"].tap() // 通过 Accessibility Title “Edit” 来定位
let predicate = NSPredicate(format: "label BEGINSWITH[cd] 'set your team details'")
let label = app.staticTexts.elementMatchingPredicate(predicate)
层级关系:
比如:let cellQuery = cells.containingType(.TextField, identifier:"Cookie")
就是找到类型是 TextField,然后 Accessibility identifier 是 Cookie 的 cell。
注意: Descendants 和 Children 还是不同的。
let allButtons = app.descendantsMatchingType(.Button) //1
let allButtons = app.buttons //2
let childButtons = navBar.childrenMatchingType(.Button) //3
上面 1 和 2 两个表达式是相同的,但是 3 只表明下一层的 Button。
Query 可以返回匹配的元素,从一个 Query 里得到元素一般有几种方法:
table.staticTexts["Groceries"]
table.staticTexts.elementAtIndex(0)
当我们使用 type 做 Query 时,其实返回的应该是元素集合。我们前面说过,UI Testing 的操作必须针对单个元素,如果是集合的话,就需要再次定位,比如通过 elementBoundByIndex
, 但是我感觉如果上下文能保证唯一性,就可以直接使用。
需要注意的是,Query 也可以返回 Query 对象,这个就像 JAVA 的 Build 模式一样,这样就串起了一个 Query chain,比如 let labelsInTable = app.tables.staticTexts
。
小贴士
层级关系和特定信息可以组合来帮助定位。
Query 只是一个 Query,只有真正需要的时候,才会执行这个 Query 然后返回结果。所以有可能你的 Query 是错误的,这个错误会在 Query 在被使用的时候才会报错。所以什么时候 Query 会被执行呢?
以下内容来自 UI Testing Cheat Sheet and Examples
如何断言一个元素是否存在?
XCTAssert(cellQuery.element.exists)
如何等待一个元素出现?
private func waitForElementToAppear(element: XCUIElement, file: String = __FILE__, line: UInt = __LINE__) {
let existsPredicate = NSPredicate(format: "exists == true")
expectationForPredicate(existsPredicate, evaluatedWithObject: element, handler: nil)
waitForExpectationsWithTimeout(5) { (error) -> Void in
if (error != nil) {
let message = "Failed to find \(element) after 5 seconds."
self.recordFailureWithDescription(message, inFile: file, atLine: line, expected: true)
}
}
}
如何点击?
UI Testing 提供了很多点击方法:
如何输入?
对于输入框,需要进行一次点击,取得输入框的焦点,所以一般:
let textField = app.textFields["Team Name"]
textField.tap()
textField.typeText("Dig Newtons")
另外中文输入,估计就困难了。
如何处理 Alert?
app.alerts["You won!"].buttons["Awesome!"].tap()
如果系统级别的 alert,貌似目前会使应用 crash。Joe Masilotti 在 Xcode GM 版本的时候提了 bug,不过正式版还是没有修复。
如何拖动滚动条?
app.sliders.element.adjustToNormalizedSliderPosition(0.7)
0.7 表示如果 slider 有 10cm 长,那么滑到 7cm 的地方。
如何从 Picker view 选择?
app.pickerWheels.element.adjustToPickerWheelValue("6-2 Formation")
** 如何点击 webview 中的链接?
tap 可以在 UIWebView 和 WKWebView 中直接使用。
app.links["Volleyball"].tap()
暂时还不清楚,UI Testing 在 HTML5 应用中的能力,这个个人觉得取决于 Accessibility。
如何拖动元素?
let joeButton = app.buttons["Reorder Joe"]
let brianButton = app.buttons["Reorder Brian"]
joeButton.pressForDuration(0.5, thenDragToElement: brianButton)
总的来说:UI Testing 的东西并不比 UIAutomation 来的丰富,而且还有很多 bug。比如我之前发现,UI Testing 不能用在 UIDocumentMenuViewController 上(问了 Joe Masilotti,他说给 Apple 提个 bug )。
Xcode 的报告一向都非常精美,可惜从来不能导出。报告包括:
又不得不提起那条底裤,我知道很多公司都没有底裤的。
按照科学的做法应该是: 单元测试-》集成测试-》验收测试
WWDC 的演讲里很明确的表示:
UI Testing 和单元测试的比较:
UI Testing 可以用于:
个人不想评价太多,关于 UI 自动化,的确给测试圈带来了活力和逼格。但是如果真要说起 ROI,可能大规模 UI 自动化会是个噩梦。选择和权衡,是需要一个公司的开发负责人和测试负责人一起商量权衡的。
本文写到这里的时候,我发现我又介绍了一个自动化工具。它并不像 WWDC 视频中介绍的那么美妙,相反有一点痛苦。从技术层面来看,没有任何新意,但是由于某种意义上,它是苹果的亲儿子,苹果对于它的发展应该会不遗余力。从 Appium 的作者 Jonathan 那边了解到,他们也会考虑切到 UI Testing 去。同时 Facebook 在 Selenium Conf 2015 上放出的 WebdriverAgent 用的是 UIAutomation 的私有 API,似乎也有在考虑 UI Testing。作为移动测试的一员,了解下它,还是非常有必要的。
感谢社区和网络上的牛人: