Xcode 7 中的 UI 测试功能

jopen 9年前

苹果公司终于决定在今年的 WWDC 上对用户界面进行加倍测试,让我们深入到 API 看看我们能发现什么.

Xcode 7 中的 UI 测试功能

背景

我从事于 IOS 测试已经有几个年头了,在进入 BeerMenus 之前,我在 Pivotal 呆了两年,Pivots, 我们更愿意这样被称呼, 严谨测试.。作为测试驱动开发公司 (或者 TDD 公司), Pivots花时间来测试每一个角落裂缝。尽管代码的覆盖率并不是最优先级的,它们很容易包含95%,并不全是这样的项目。


Cedar

回到 Xcode 4,测试 iOS 应用的唯一方式就是 OCUnit,其遗留了许多有待改进之处。Pivots 利用了它们在 行为驱动开发(behavior-driven development)框架中的专业观点着手创建它们自己的测试套件 -  Cedar 诞生了.

Cedar 是一个优秀的框架,用于创建 BDD 风格的测试,完全支持匹配器和伪装。而 Xcode 7 还没有完全的支持,Pivotal 在一个分支上跟进他们的工作,预计很快就会有东西要发布了。自从尝到 TDD 的甜头之后, 我使用 Cedar 测试过我所工作过的每一个 iOS 应用。我甚至开始把它用在功能测试控制器上,但那是下次的主题了。

单独使用 Cedar 不能给与我在使用 Ruby on Rails 时所日渐赞赏的端到端覆盖。我不能可靠地在几个屏幕之间进行触摸操作,让应用运作起来。可以理解的是,Cedar 并不用为那个而被创建出来的。为了填补这个空缺,许多玩家都已经创建了他们自己的第三方功能测试框架。

第三方测试

FrankKIFSubliminal, Apple 的 UIAutomation,我把他们都试了一遍。你要是希望了解更多可以访问我的故障特征测试框架。它不是开发者的失败,而是因为 Apple 对待测试只有有限的开放性。这使得这些框架有一系列的补丁,而在这些补丁之上,这些框架不外乎都成为了一堆破碎的工具。

没有涉及到的更多细节:

  • Frank 一直被遗弃。

  • KIF 已经与主要的 iOS 修订版本决裂。

  • Subliminal 不能在命令行中可靠地运行。

  • UIAutomation 是用 JavaScript 和 clunky 写的。

我在 Pivotal 工作的时候,有六个分离的 app,我尝试对每个不同应用使用不同的框架。我甚至对 KIFFrank 捐助,因为我希望这些框架成功。不幸地是,它们不可能离开 Apple 的支持。

WWDC 2015

Xcode 7 中的 UI 测试功能

今年的 WWDC 带来了好消息:在各平台进展状态介绍环节对用户界面测试进行了介绍。

Xcode 7 引入了用户界面测试,旨在确保您在代码中所做的修改,不会向您的用户呈现出乎意料的效果。

Wil Turner 和 Brooke Callahan 在第 406 节展示了新框架,Xcode 中的 UI 测试。如果你还没有看过它,建议去看一看。他们演示了一个简单的任务管理应用,对工具中的一些 API 和功能进行了高亮显示。

Xcode 7 中的 UI 测试功能

UI 测试的重头戏是“记录”。在你有了一个要位置工作的应用程序之后,点击一个大红圈,然后开始在你的 app 里面到处点击。当你正在与 app 交互时,代码会被自动生成出来以重现你的操作流程。理论上,之后你就能够运行新创建的测试,并看着 app 重复你之前的动作。

UI 测试实战

自卖自夸是一回事,在真实世界中这个框架会起作用吗?

文档

Xcode 7 中的 UI 测试功能

关于这个新框架你可能知道的首要事情之一就是文档的缺乏.  iOS 9 和 Xcode 7 都没有包含 UI 测试的任何官方的文档。幸运的是,新的 XCU* 类中的大多数都拥有良好的注释的头文件。借助于 appledoc,我可以把这些头文件拖入 Xcode 兼容 Dash 的文件集合中。只要把资源库 clone 下来,将文件打开;默认的文档浏览器就会添加这些新的 API。你还可以在线查看文档

UI 测试位于 XCTest 框架,添加了4个新的类,1个新的协议,以及3个新的常量。

启动 App - XCUIApplication

这是用来启动你要测试的应用程序的主要拉钩。你可创建一个新的实例,[[XCUIApplication alloc] init], 然后在它上面调用 -launch。 如果你向应用添加了一个新的 UI 测试目标,就会在模板里面看见一个示例。在启动之前,你可以通过设置 -launchArguments 或者 -launchEnvironment  属性来分别设定一个特殊的参数或者环境变量。这样会创建一个漂亮的布局,例如,向你的 HTTP 客户端告知,在测试时访问一个模拟的服务器.

XCUIApplication *app = [[XCUIApplication alloc] init];  app.launchArguments = @[ @"USE_MOCK_SERVER" ];  [app launch];

你还可以 -terminate 你的 app,但我还没有发现过对那个东西的使用。

访问设备 - XCUIDevice

你可以使用 +sharedDevice 创建这个对象的一个新实例。唯一可以被公共的访问到的属性是-orientation,它会返回普通的 UIDeviceOrientation 枚举值。虽然这是一个可读可写的属性,但是对他进行了设置并不会对模拟器的界面进行更新。我不确定这是不是我正在使用的 beta 版的一个 bug。

元素查询 - XCUIElementQuery

这将是 UI 测试中使用率最高的一类,因它可用来构建查询以定位元素。还记得 UI 自动化里的 app.tableViews[0].cells[0] 吗?二者极其相似。

[[app.tables.element.cells elementAtIndex:4] tap];

每次调用堆栈都会返回一个可被链接在一起的查询对象,这就让控制应用程序变得更加精确。提到-element(元素),你便会说“我知道这儿只有一个,我要的就是这个”。你还可以通过特定的标签来访问堆栈中的对象。

[app.tables.element.cells[@"Call Mom"].buttons[@"More"] tap];

这种便捷的方式将通过 -elementMatchingType:identifier: 和 XCUIElementTypeAny,以及你传递的字符串来实现。如果想要更精确地控制所有选择,通过元素查询 XCUIElementType 将是最佳的方式。

与元素进行交互 - XCUIElement

元素是封装了需要在一个应用程序中动态放置一个用户界面的相关信息的对象。元素用查询的方式被描述。 当调用到一个事件API, 元素就会被解析到。如果发现有0个或者多个匹配,就会冒出一个错误。

创建一个XCUIElementQuery的方式,会给予你元素的引用。除非你真的到了要与其交互的时候,框架是不会实际去应用的层级中找寻它们的。 这样为你带来了保持干净的测试的好处——你可以保留一个查询的引用,在测试的不同地方重用它。元素只会在交互期间才会被查找,这样就为你节省了再一次深入获取的花费。

交互都被进行了恰当的命名,会像你所期望的那样执行动作。例如,在 iOS 中你可以使用-tap,-pressForDuration:,-swipeUp, 以及-twoFingerTap。 如果你测试的是一个 OS X 应用,你可以对应地使用-click*交互. 你甚至还可以使用+performWithKeyModifiers:block:来模拟持续的按下⌘按键.

一个测试框架里怎么可能没有断言?由于 UI 测试只是 XCTest 的扩展,所有已有的断言仍然管用,你仍然可以使用像 XCTAssert(),XCTAssertEquals(),和 XCTAssertNotNil() 这样的断言宏。

检查某个元素是否存在

一个快速的方法就是在一个元素对象上进行非空断言测试:

XCTAssertNotNil(app.buttons[@"Submit"]);

但是,elementMatchingType:identifier:总是返回一个对象,在这这个函数解析完之前,由它来决定了一个元素是否真的与查询相匹配,这说明这个测试总是通过的。作为替代,在断言中通过在元素上调用 -exist 方法来包装一下,像这样:

XCTAssert(app.buttons[@"Submit"].exists);

另一个建议就是自始至终都不用断言,那样在运行测试的时候如果解析出的查询结果并不能匹配到一个元素则测试就会自动失败。如果想这样做,别忘了在 setup 方法里关闭(注释)[XCTestCase -continueAfterFailure] 这项。

调试

我经常用到的调试方法是打断点或者在控制台输出对象的内容(我知道这难以置信)。在测试代码中设置断点并且命中断点,调试器就会定位到问题所在。但是你不能在一个 app 的代码里加装调试器。由于打印的日志语句会在控制台里显示,所以要利用这个来调试。

Xcode 7 中的 UI 测试功能


当在控制台输出 XCUIElementQuery 的结果时,马上就会出现许多无关的干扰输出。试想一个表里有标着"1"到"5"标签的五个单元。触摸带有标签"3"的单元时候你可以打印如下的日志(为了清晰显示,这里忽略一些关键字输出):

(lldb) po app.tables.element.cells[@"Three"]  Query chain:   →Find: Target Application    ↪︎Find: Descendants matching type Table      Input: {        Application:{ {0.0, 0.0}, {375.0, 667.0} }, label: "Demo"      }      Output: {        Table: { {0.0, 0.0}, {375.0, 667.0} }      }      ↪︎Find: Descendants matching type Cell        Input: {          Table: { {0.0, 0.0}, {375.0, 667.0} }        }        Output: {          Cell: { {0.0, 64.0}, {375.0, 44.0} }, label: "One"          Cell: { {0.0, 108.0}, {375.0, 44.0} }, label: "Two"          Cell: { {0.0, 152.0}, {375.0, 44.0} }, label: "Three"          Cell: { {0.0, 196.0}, {375.0, 44.0} }, label: "Four"          Cell: { {0.0, 240.0}, {375.0, 44.0} }, label: "Five"        }        ↪︎Find: Elements matching predicate ""Three" IN identifiers"          Input: {            Cell: { {0.0, 64.0}, {375.0, 44.0} }, label: "One"            Cell: { {0.0, 108.0}, {375.0, 44.0} }, label: "Two"            Cell: { {0.0, 152.0}, {375.0, 44.0} }, label: "Three"            Cell: { {0.0, 196.0}, {375.0, 44.0} }, label: "Four"            Cell: { {0.0, 240.0}, {375.0, 44.0} }, label: "Five"          }          Output: {            Cell: { {0.0, 152.0}, {375.0, 44.0} }, label: "Three"          }


这样看起来对于框架如何定位元素就非常清晰了。提示:在第一个输入/输出循环中的 -table 方法返回了填充在这个 iphone6 模拟器屏幕里面的列表(table)。再往下就是 -cells 方法返回了所有的单元(cell)。最终,文本查询仅仅在最后返回了一个元素。如果你没有在输出的最后看到带"Output"关键字的输出,说明框架没有找到你想要的元素。

它准备好了吗?

作为 Xcode 7 Beta 2,UI 测试做出了承诺。Xcode 7 Beta 2 是可靠的,它的查询引擎功能十分强大,并且在未来比任何第三方库都有用。

然而,它还有些奇怪的地方。目前,它还没办法等待元素的出现或者消失。这就以为着,在你继续工作之前,你无法等待网络活动的完成。我用尽了所有异步测试方法去循环好用但是老旧的 sleep()。

不幸的是,只要你搞砸了任何 App 的时序,Xcode 就开始卡壳,并且测试就会崩溃。

Xcode 试图去记录那些没有注册的特定事件。例如,任何与 WKWebView 的交互都必须手写。幸运的是当请求位置授权时,会与系统警报进行交互。我希望这些问题会在下个 beta 版本发行时能被 GM 修复。

总而言之,我认为每个人都至少应该在自己的项目上使用 UI 测试。它提供了一个验证你的应用程序的新的方法,而且它还提供了第二层测试说明,App 商店中的任何 App 都能从中受益。让我兴奋的是,未来它将整合进我每天工作的项目中,并且十分期待社区会如何使用它。