测试代码并不属于应用目标的一部分,其目的在于确保应用运行与期望保持一致。测试可以分为如下两类:
单元测试
单元测试会在内部执行应用目标,这是从代码的角度来看待的。比如,单元测试可能会调用应用目标代码的某个方法,传给其若干参数,然后看看是否每次都能返回期望的结果,不仅仅在正常情况下,还要看不正确或极端输入情况下是否也能如此。
界面(UI)测试
界面测试(Xcode 7新增的功能)会在外部执行应用,这是从用户的角度来看待的。这种测试会让应用通过一系列用例场景,用手指轻拍界面上的按钮,观察结果并确保界面行为与期望保持一致。
在理想情况下,测试应该伴随着应用开发的过程来编写和运行。在编写实际代码前编写单元测试会更好一些,可作为实现算法的一种方式。在确定代码通过了测试后,可以继续运行这些测试来检测在开发过程中是否引入了Bug。
测试会被打包到项目的一个单独的目标当中(参见第6章)。借助应用模板,可以在创建项目时添加测试目标:在第2个对话框(“Choose options”,即命名项目时)中,可以勾选Include Unit Tests、Include UI Tests,或二者都勾选上。此外,可以随时轻松创建新的测试目标:创建一个新的目标,并指定iOS→Test→iOS Unit TestingBundle或iOS UI Testing Bundle即可。需要显式运行测试才行。可以在测试导航器(Command-5组合键)与测试类文件中轻松管理并运行测试。
测试类是XCTestCase(它本身又是XCTest的子类)的子类。测试方法是测试类的实例方法,它没有返回值并且不接收参数,并且名字以test开头。测试目标依赖于应用目标,这意味着在编译和构建测试类之前,我们需要先编译和构建应用目标。运行测试也会运行应用,测试目标的产物是个包,它会在应用启动时加载到应用中。
一个测试方法需要调用一个或多个测试断言;在Swift中,这些断言都是全局函数,名字以XCTAssert开头。请参阅Apple文档Testing With Xcode的“Writing Test Classes and Methods”章节了解完整的函数列表,具体位于“Assertions Listed by Category”一节下。与相应的Objective-C宏不同,Swift测试断言函数并不接收格式化字符串(NSLog所采取的方式);每个函数都接收一个简单的消息字符串。在Swift中,标记为“针对标量”的测试断言函数并非真的如此,因为Swift中是不存在标量的(相对于对象来说):它们会应用于使用了Equatable或Comparable的类型。
测试类还可以包含一些辅助方法,这些方法会被测试方法所调用。此外,可以重写从XCTestCase继承下来的4个特殊方法:
setUp类方法
只会调用一次,并且在类中所有测试方法执行之前调用。
setUp实例方法
在每个测试方法调用前调用。
tearDown实例方法
在每个测试方法调用后调用。
tearDown类方法
只会调用一次,并且在类中所有测试方法执行之后调用。
测试目标也是个目标,其产出是个包,其构建阶段类似于应用目标。这意味着,如测试数据等资源可以放到包中。可以通过setUp加载这些资源;通过测试类获得对包的引用:作为NSBundle(forClass:self.dynamicType)。
测试目标也是个模块,就像应用目标一样。为了能够使用应用目标,测试目标需要将应用目标以模块的形式导入进来。为了克服私有性限制,import语句前面应该加上@testable特性;该特性是Xcode 7中新引入的,它会将应用目标中的internal(显式或隐式)临时改为public。
下面编写并运行一个单元测试方法,这是使用的是Empty Window项目。为ViewController类添加一个没有实际意义的实例方法dogMyCats:
func dogMyCats(s:String) -> String { return /"/"}
方法dogMyCats接收一个字符串并返回字符串/"dogs/"。但此时却并非如此;它返回一个空字符串。这是个Bug。现在编写一个测试方法以找出这个Bug。
在Empty Window项目中,选择File→New→Target并指定iOS→Test→iOS Unit Testing Bundle。将产品命名为EmptyWindowTests;待测试的目标就是应用目标。单击Finish。在项目导航器中会新建一个分组EmptyWindowTests,它包含一个测试文件EmptyWindowTests.swift。该文件中有一个测试类EmptyWindowTests,其中包含两个测试方法的桩:testExample与testPerformanceExample;将这两个方法注释掉。我们打算使用一个会调用dogMyCats的测试方法将其替换,该方法会对结果作出断言:
1.在EmptyWindowTests.swift顶部导入XCTest的地方,也需要导入应用目标:
@testable import Empty_Window
2.请在EmptyWindowTests类的声明中添加一个实例属性,用于存储ViewController实例:
var viewController = ViewController
3.编写测试方法。测试方法的名字要以test开头!我们将其命名为testDogMyCats。它可以通过self.viewController访问ViewController实例:
func testDogMyCats { let input = /"cats/" let output = /"dogs/" XCTAssertEqual(output, self.viewController.dogMyCats(input), /"Failed to produce (output) from (input)/")}
如果向老项目中添加单元测试,你可能需要做一些额外的配置。为了确保可以通过@testable特性导入应用目标,请在构建设置中找到Enable Testability并确保在调试配置中将其设为Yes。此外,编辑方案,确保构建动作在测试动作发生时只会构建测试目标。
现在可以运行测试了!有多种方式可以做到这一点。切换到测试导航器,你会看到里面列出了测试目标、测试类与测试方法。将鼠标指针悬停在任意名字上,这时会在右侧弹出一个按钮。通过单击恰当的按钮,可以运行每个测试类中的所有测试、EmptyWindowTests类中的所有测试,还可以单独运行testDogMyCats测试。不过请等一下,还有呢!回到EmptyWindowTests.swift,在类声明与测试方法名左侧的边列中有一个菱形指示器;还可以单击这个指示器运行测试,既可以运行这个类中的所有测试,也可以运行单个测试。要运行所有测试,还可以选择Product→Test。
下面运行testDogMyCats。应用目标已经编译并构建完毕;测试目标亦如此(如果其中有任意一步失败了,测试就将无法进行,我们需要回过头来解决编译错误或构建错误)。应用会在模拟器中启动,测试也将运行。
测试失败了(我们其实知道会失败)!错误说明会出现在代码中失败断言的旁边,以及问题导航器与日志导航器中。此外,测试导航器中testDogMyCats旁边、问题导航器、日志导航器与EmptyWindowTests.swift类声明旁边与testDogMyCats的第一行还会出现红色的X标记。
现在来修复代码。在ViewController.swift中,将dogMyCats的返回值由空字符串修改为/"dogs/"。再次运行测试。通过!
最近运行的测试会列在报告导航器中。如果选择了其中一个,编辑器就会显示出两个窗格。测试窗格会以简单的大纲形式列出成功与失败的测试,包括断言失败消息的文本。日志窗格则会列出更为详尽的信息;展开后会看到运行过的测试的完整控制台输出,包括测试代码中所输出(print)的原始调试消息。
当测试失败时,你可能希望在断言失败之处暂停。要做到这一点,请在断点导航器中单击底部的“+”按钮并选择Add Test Failure Breakpoint。这类似于异常断点,它会在报告失败前,在测试方法中断言失败那一行处暂停。接下来可以切换到被测试的方法,对其进行调试,查看其变量,从而找出失败的原因所在。
有一个很有用的特性可以帮助你在方法与调用该方法的测试间切换:当选中方法中的某些代码时,跳转栏中的Related菜单就会包含进测试调用者。对于辅助窗格中的Tracking菜单来说亦如此。
在该示例中,创建了一个新的ViewController实例来初始化EmptyWindowTests的self.viewController。不过,如果测试需要引用现有的ViewController实例该怎么办呢?这与iOS编程中频繁出现的实例引用是一个问题。测试代码运行在一个包中,它会被注入运行着的应用中。这意味着它能看到应用的全局信息,如UIApplication.sharedApplication()。可以通过它得到所需的引用:
if let viewController = (UIApplication.sharedApplication.delegate as? AppDelegate)? .window?.rootViewController as? ViewController { // ...}
将测试方法组织到测试目标(套件)与测试类中在很大程度上是为了方便;这会对测试导航器的布局以及哪些测试会一起运行产生影响,同时每个测试类都有自己的属性,自己的setUp方法等。要创建新的测试目标或测试类,请单击测试导航器底部的“+”按钮。
除了刚才介绍的简单的单元测试类型,还有另外两种形式的单元测试:
异步测试
可以在一个耗时操作执行完毕后回调测试方法。在测试方法中,可以通过调用expectationWithDescription来创建一个XCTestExpectation对象;然后初始化一个接收完成处理器的操作,调用waitForExpectationsWithTimeout:handler:。这样会出现下面两种情况之一:
操作完成
完成处理器会被调用。在完成处理器中,可以执行与操作结果相关的任何断言,然后调用XCTestExpectation对象的fulfill。这会导致超时处理器得到调用。
操作超时
超时处理器会被调用。这样,超时处理器都会得到调用,可以执行必要的清理工作。
性能测试
可以测试成功操作的速度。在测试方法中调用measureBlock,在块中做一些事情(可以执行很多次,从而得到合理的时间度量样本)。如果块中涉及度量不想包含的创建与清理工作,那就可以调用measureMetrics:automaticallyStartMeasuring:forBlock:,然后将块的核心包装到startMeasuring与stopMeasuring中。
性能测试会执行块多次,记录每次运行时间。首次执行性能测试时会失败,不过却建立了基准度量。在随后的运行中,如果标准偏差距离基准太远,或平均时间变得过长,测试都会失败。
现在来看看界面测试。假设Empty Window界面上依旧有一个按钮(第7章),它有一个动作方法挂接到了ViewController方法上,会弹出一个警告框。我们会编写一个测试,轻拍按钮并确保会弹出警告框。向项目添加一个iOS UI Testing Bundle,命名为EmptyWindowUITests。
界面测试代码是基于可访问性的,该特性可以描述屏幕的界面,然后以编程的方式操纵它。它涉及3个类:XCUIElement、XCUIApplication(XCUIElement的子类)以及XCUIElementQuery。在很大程度上,你不必了解这些类,因为可访问性动作是可记录的。这意味着可以通过执行构成测试的实际动作来生成代码。下面就来试一下:
1.在testExample桩方法中,创建一个新的空行,并将插入点置于其中。
2.选择Editor→Start Recording UI Test(此外,还可以使用项目窗口底部调试栏上的Record按钮)。应用会在模拟器中启动。
3.轻拍界面上的按钮。当警告框出现时,轻拍OK将其关闭。
4.回到Xcode,选择Editor→Stop Recording UI Test。选择Product→Stop会停止在模拟器中运行。
这会生成如下代码(假设界面按钮上的文字是“Hello”):
let app = XCUIApplicationapp.buttons[/"Hello/"].tapapp.alerts[/"Howdy!/"].collectionViews.buttons[/"OK/"].tap
显然,app对象是个XCUIApplication实例。buttons与alerts等属性返回XCUIEle-mentQuery对象。对该对象进行下标计算会返回一个XCUIElement,接下来可以向其发送tap等动作方法。
现在运行测试,单击testExample声明边栏上的菱形图标。应用会在模拟器中启动,手指会执行我们之前所执行的相同动作,轻拍界面上的第1个按钮,当警告框出现后,轻拍OK按钮,警告框就会消失。测试结束,应用在模拟器中停止运行。测试通过!
不过,重要的事情在于如果界面停止响应,那么测试就不会通过。比如,在Main.storyboard中,选择按钮,在属性查看器下方的Control中取消勾选Enabled。按钮依旧在那儿,不过却无法轻拍它;我们破坏了界面。运行测试。如期望一般,测试失败了,报告导航器会给出原因:当进入轻拍“OK”按钮这一步时,我们首先要进行查找“OK”按钮的操作,尝试两次后失败了,因为根本就没有警告框。报告还会截屏,这样我们就可以检测到测试过程中界面的状态了。将鼠标悬浮在“OK”按钮上,一只眼睛的图标会出现。单击它,截图会展示出该时刻的界面,清晰地显示出禁用的界面按钮(没有警告框)。
再次启用按钮来修复这个Bug。如果现在选择Product→Test,那么测试套件中的所有测试都会运行,包括单元测试与界面测试,它们都会通过。这个应用很简单,不过却是可用的!
如前所述,界面测试依赖于可访问性。标准的界面对象是可访问的,不过你所创建的其他界面可能未必如此。在nib编辑器中选择一个界面,在身份查看器中查看其可访问性。在模拟器中运行应用,选择Xcode→Open Developer Tool→Accessibility Inspector来实时查看鼠标指针下方内容的可访问性。要了解如何向界面对象添加有用的可访问性特性,请参考Apple的Accessibility Programming Guide for iOS。