UI Testing in Xcode
这是2015年的WWDC session,因为对UI测试有一些困惑,所以特地看了一遍。
session地址
UI 测试最低支持iOS 9。
引用一段苹果开发指引里的话:
UI testing differs from unit testing in fundamental ways. Unit testing enables you to work within your app's scope and allows you to exercise functions and methods with full access to your app's variables and state. UI testing exercises your app's UI in the same way that users do without access to your app's internal methods, functions, and variables. This enables your tests to see the app the same way a user does, exposing UI problems that users encounter.
UI测试和单元测试是不同的,单元测试是在app内部运行,你有完整的权利去运行与访问方法和变量。而UI测试是让你用和用户一样的角度去使用和测试app,因此你没有权限去访问内部方法函数等。
核心技术

主要就是 XCTest 和 Accessibility,Accessibility主要为残障人士准备。

主要流程
- Adding a UI testing target
- Using recording
- Finding UI elements
- Synthesizing user events
- Adding validation with XCTAssert
这里引申到我在做 UI 测试的时候遇见的两个问题:如果我的App每次都要登陆,那么是不是我每个测试都得先做一遍登陆操作才能继续?如果我的某个功能点藏的层级很深,我是不是要一步步点进去?
先回答第二点:以我目前的了解来说,UI测试是以用户的视角操作app,因此我们不可能直接引入某个模块,直接传参进行调用,而是必须像用户一样一层一层点击,让app自行收集这些参数去调用。说白了在UI测试的时候,你要假设你对代码是 0 可知的。
APIs
Three new classes
- XCUIApplication
- XCUIElement
- XCUIElementQuery
UI测试引入了这三个新东西,以下将会做详细阐述。
XCUIApplication
Proxy for the tested application
- Tests run in a separate process
每次UI测试都是单独开线程,和工程代码隔离,生命周期也和工程代码独立,所以能在应用启动和终结的时候有更多的操作和控制(setup和teardown)
Launch
- Always spawns a new process
- Implicitly terminates any preexisting instance
每次launch都是新的实例,是干净的状态,所以上面提到的问题如果不想每次测试都登陆一次,可以在setup里登陆好。而如果是一系列相关联的测试,可以一次性测试完。
每次调用launch都会创建一个新的实例并终结之前的实例。
Starting point for finding elements
这也是查找元素的起点。
XCUIElement
Proxy for elements in application
Types
Button, Cell, Window, etc.
Identifiers
Accessibility identifier, label, title, etc.
注意到大多数元素都是通过Accessibility identifier去查找的
Most elements are found by combining type and identifier
通过类型和易用性id我们可以查找到绝大多数元素

Element Uniqueness
Every XCUIElement is backed by a query
每一个XCUIElement都是一个查询的返回,并且这个必须有且只有一个匹配项
Query must resolve to exactly one match
- No matches or multiple matches cause test failure
- Failure raised when element resolves query
如果无法返回唯一查找结果,就会报出一个failure
Exception
``exists`` property
XCUIElementQuery
API for specifying elements
更复杂的方式去查找指定元素
Queries resolve to collections of accessible elements
- Number of matches:count
- Specify by identifier: subscripting
- Specify by index:elementAtIndex()
eg.
Type | eg |
---|---|
Subscripting | table.staticTexts[“Groceries”] |
Index | table.staticTexts.elementAtIndex(0) |
Unique | app.navigationBars.element |
Accessibility and UI Testing
Debugging tips
Not accessible
- Custom view subclasses
- Layers, sprites, and other graphics objects
这个在下文有示例解释
Poor accessibility data
Tools
- UI recording Accessibility inspectors
上面这一段主要讲的是一些常见的无法进行 UI 测试的情况
常见的原因包括控件不可达:UI测试只能访达显示出来的 UI 组件、自定义控件未做 Accessibility 适配等。
Accessibility inspectors 就是检查
Accessibility 的工具。
ScreenShots

视频中提到了一个很有意思的功能,具体可参见30分钟处。
他在table中添加了一个Cookies,并且在之后添加了第二个Cookies。
随后的测试中因为UI 测试无法准确找到对应的组件而失败,这时报错信息是有两个Cookies。
然后,我们在 Report navigator 里可以看到错误发生的时候的ScreenShots,这是一个很有意思的功能。在跑测试的时候一般我们不会盯着app的运转,而当测试失败的时候,这些ScreenShots就可以帮我们回溯测试的过程,看看发生错误的时候app处于什么样的一个状态。
Record
还有一个功能是测试中的断点record,如果你在record的时候有一些冗余操作或者遗漏操作,你可能想整段删掉重新录入,但是你其实可以在恰当的位置打个断点,然后点击record,就可以继续录入。
小坑(待确认)

这个地方为什么用cells.count 而不是exist呢,是因为这里有个小坑,目前不知道这个坑是否依然存在

就是当cell元素被删除之后,它重新查找了元素,这里用的是index[0],删除之前是Apple,删除之后指向了Orange,所以exists判定仍然存在,只是不是Apple了
自定义元素
40分的时候还演示了一个不符合accessibility的元素是如何不能被查找到

这个红色待色块是一个custom view,并不是一个button所以查找不到,尽管我们的实现使得它的表现看起来像是一个button。因此点击 redView 的时候实际上record的是点击了cell[0],也就是cell在tableView中所在的位置。
那么怎么使得它能够符合accessibility的标准呢?

Enable并且指定它是button,就可以查找访问了
Attention
-
需要注意的是 UI 测试有一个不起眼的坑:
如果在虚拟机上运行测试,有一个选项是把实体键盘链接到虚拟机中(见下图),因此虚拟机中的键盘是不会弹出的,如果我们在录制的时候使用了键盘操作,是不会被记录下来的,因此就会测试出错。比如测试的时候使用软键盘按了删除键,测试的时候链接了实体键盘就会测试失败,报找不到删除键。又如测试的时候链接了实体键盘,那么操作不会被记录,录制下来的操作就会和实际操作不符。
image.png
总结
UI Testing 是 Unit Testing 的补充,
Unit Testing 用来测试 Models 和Controllers,而 UI Testing 则是我们针对 View 的有力工具,以 user 的视角
Accessible 和UI Testable 因此而捆绑在一起,UI Testable 的应用也就会对残障人士更友好