大约两星期前,苹果发布 Xcode 7 的第一个测试版,附带闪亮新功能,如内置的 UI 测试功能。除了 Swift,这是最容易让我兴奋的功能。我迫不及待地尝试了一番。UI 测试并不是什么新技术,早在 2011 年就已经有 KIF。这是一个伟大的工具,但也有其局限性:速度慢,不是 100% 可靠的,而且它依赖于私有 API。因此,官方的解决方案似乎是个不错的主意。
该解决方案集成到苹果的单元测试框架 XCTest 中。它的使用与 KIF 很相似。然而,它是建立在新版本的 UIAutomation 上,更稳定可靠。因为它内建在 Xcode 中,具有更强大的功能,如 UI 记录和自动截图,让它变得更加方便,更加有用。
在下面的章节,我会谈论两个框架之间最大的区别。
灵活性
那么,我说干就干,开始为 ImagePickerSheetController 重写 UI 测试代码,我的开源项目之一。它本质上是在 iOS iMessage 选择图片的自定义操作表(action sheet)。
我第一个想重写的是一个非常基本的测试。简单地测试下当点击取消按钮时,控制器是否会撤回。
1 2 3 4 5 6 7 |
func testDismissal() { let imageController = ImagePickerSheetController() imageController.addAction(ImageAction(title: “Cancel”)) rootViewController.presentViewController(imageController, animated: true, completion: nil) tester().tapViewWithAccessibilityLabel(“Cancel”) tester().waitForAbsenceOfViewWithAccessibilityIdentifier(ID) } |
虽然这看起来非常简单,但这使用 XCTest 不可能实现。问题是,Xcode 的 UI 测试不允许访问实际的应用。API 提供的只是充当个代理。试图直接检索一个视图而不是使用 XCUIElement,这是行不通的,XCUIElement 只是用来展示视图。
在 WWDC 开发工具实验室有人告诉我,XCTest 的主要目标是让应用更容易测试。使用这个框架还是很明智的,于是我开始重写我的一个叫 Crimson 的应用测试。
Crimson 键盘显示了字母“E”的不同声调。
经过几个基本测试后,我开始对“声调视图”进行操作。它显示了一个特定字母的不同声调。我想测试的是下面两点:
- 当视图显示时,第一个字母,在这里是“e”,应被选中
- 声调视图的颜色应与键一致
我初以为不可能写这测试。和之前一样,你不能只通过审查实际的视图来检查属性。在我仔细查阅文档一两分钟后,我偶然发现 XCUIElement 的实用属性 value:
1 2 |
/*! The raw value attribute of the element. Depending on the element, the actual type can vary. */ var value: AnyObject? { get } |
它所做的就是返回底层 UI 元素的 accessibilityValue。这让测试访问属性成为可能。所以,我需要唯一做的就是设置声调视图的 accessibilityValue:
1 2 3 4 5 6 7 |
class AccentView: UIView { var selectedLetter: String { didSet { accessibilityValue = selectedLetter } } } |
直到我想检测颜色,这一切都进展顺利。由于 accessibilityValue 是字串类型而不是字典,它不可能再访问第二个属性。除此之外,accessibilityValue 就像是用户面对的 accessibilityIdentifier 一样。把 accessibilityValue 设置成模糊值,如一种颜色,是种非常不好的做法,而且完全违背了目的。
因此本次测试也是不可能实现的。虽然很容易检查元素的 frame 和 label 值,但它几乎不可能用来测试其他东西。
性能
为了比较两种框架的性能,我为示例项目写了 3 份测试。为了模拟一个合理的测试组,每个测试都执行 20 遍,使得总共有 60 个测试。
对于第一次测量,我实现了文档中的测试。对于 XCTest,这意味着我会在每次测试后重新运行该应用。KIF 不支持这样,这让它在第一个比较中显得比较快速。
第二次测量通过返回导航而不是重新运行,重置了目标应用的状态。
第三次测量,我又取消了所有的动画。
因为 Xcode 7 不断崩溃,我不得不将迭代的次数减少到 5,然后将结果乘以 4。
正如我们在第一次比较中看到的,为每个规格重新运行应用,使得该测试组极其缓慢。令人惊讶的是 UI 测试模版建议使用这种重置方式,而不是通过导航,因为在大多情况下,没必要使用这种方式。
第二次比较显示,在正常情况下,XCTest 比 KIF 稍微快一点。需要注意的是,在一个比较大的且每天需要多次评估的测试组中,10 秒就可以省下大量的时间。
最后的比较中,XCTest 让人眼前一亮。然而,并不是每个项目都可以禁用所有动画。试想一个带许多自定义过渡效果的项目,测试组还是应确认过渡的正常运行。
对此的解决方案就是,把测试组分成不同运行环境的类,以控制应用的配置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class UITests: XCTestCase { private var launched = false let app = XCUIApplication() override func setUp() { super.setUp() continueAfterFailure = false launchIfNecessary() } private func launchIfNecessary() { if !launched { launched = true app.launchEnvironment = [“animations”: “0”] app.launch() } } func testDetail() { ... } func testPopover() { ... } func testActionSheet() { ... } } |
然后,AppDelegate 根据运行环境进行配置。
1 2 3 4 5 6 7 8 9 10 |
class AppDelegate: UIResponder, UIApplicationDelegate { func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { if NSProcessInfo.processInfo().environment[“animations”] == “0” { UIView.setAnimationsEnabled(false) } ... return true } } |
这可以大大减少你等待测试通过的时间。
总结
XCTest 可以让我们很容易编写简单的 UI 测试。而想要更高级的测试则是很困难甚至是不可能的。而 KIF 则更为灵活些。
然而,相比 KIF,XCTest 在查找 UI 元素上稍快些。在禁用动画的情况下,XCTest 是非常快。
虽然我希望 Xcode 内置的 UI 测试可以帮助我重写所有项目测试,但是我放弃了。我根本无法找到可以编写所有测试的方式。