当你为一个项目添加测试target时,Xcode会在test导航中显示测试类和方法。在测试target中包含测试类,测试类中包含测试方法。本章解释了如何创建测试类和编写测试方法。
测试target,测试bundle和测试导航
在了解如何创建测试类前应先了解测试导航。如何使用测试导航是创建测试和使用测试的重点。
往项目中添加测试target会默认创建一个测试bundle。测试导航列出了项目中所有测试bundle的源代码组件,并在层级列表中显示测试类和测试方法。下图是一个项目的测试导航图,有两个测试target,显示了测试bundle的嵌套层级结构、测试类和测试方法。
测试bundle可以包含多个测试类,你可以使用测试类将测试分成相关的组,以功能或组织划分。例如,对于计算器示例项目,你可以创建BasicFunctionsTests
, AdvancedFunctionsTests
和DisplayTests
类,组成Mac_Calc_Tests
测试bundle。
某些类型的测试可能共享某些setup和teardown,使这些测试组成一个类,一个setup和teardown方法可以减少你为每个测试方法写的代码。
创建测试类
注意:本章重点介绍单元测试类和方法。创建UI测试target、类和方法,以及与使用单元测试不同的地方,参见用户界面测试( User Interface Testing)。
你可以使用测试导航中的添加按钮(+)创建新的测试类。
你可以选择添加单元测试类或UI测试类。在选择其中一个之后,Xcode显示已选中的模板。在下图中“新单元测试类”高亮显示。点击下一步进行你的选择。
添加的测试类名称默认为TestClassName.m,可以在配置表中修改测试类名称,再添加到项目中。
注意:所有测试类都是
XCTestCase
的子类,该类由XCTest 框架提供。
尽管Xcode默认为你的项目测试target中添加测试类实现文件,你可以将你的文件添加到任何你选择的项目中。当按下下一步,标准Xcode添加文件表单遵守你的配置。
使用添加文件表单与添加新文件到项目中类似。有关如何使用添加文件表单,参见添加现有文件或文件夹( Adding an Existing File or Folder)。
注意:当你创建一个新项目时,默认为你创建一个测试target和相关测试bundle,且名称由项目名称派生出来。例如,创建一个新的项目叫
MyApp
,自动生成的测试bundle叫MyAppTests
,测试类叫MyAppTests
,关联一个名为MyAppTests.m
的实现文件。
测试类结构
测试类有以下基本结构:
<pre><code>
#import <XCTest/XCTest.h>
@interface SampleCalcTests : XCTestCase
@end
@implementation SampleCalcTests
-
(void)setUp {
[super setUp];
// Put setup code here. This method is called before the invocation of each test
method in the class.
}
-
(void)tearDown {
// Put teardown code here. This method is called after the invocation of each
test method in the class.[super tearDown];
}
-
(void)testExample {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct
results.
}
-
(void)testPerformanceExample {
// This is an example of a performance test case.
[self measureBlock:^{
// Put the code you want to measure the time of here.
}];
}
@end
</pre></code>
这个例子中的测试类用 Objective-C 实现的,同样也可以用Swift实现。
注意:本文中的实现例子统一用Objective-C实现的。
使用XCTest完全兼容Swift实现测试方法。也兼容Swift和Objective-C跨语言实现的功能。
注意,实现包含setup和teardown作为基本实现,这些方法不是必须的。如果类中的测试方法需要相同的代码,你可以自定义setup和teardown包含它。你添加的代码会在每个测试方法之前或之后执行。你可以随意添加自定义setup (
+ (void)setUp
) 和teardown (+ (void)tearDown
) 方法,在类中测试方法之前或之后执行。
测试执行流程
默认情况下,运行测试,XCTest查找所有测试类,运行每个类的所有测试方法。(所有继承XCTestCase的测试类。)
注意:有选项可以设置XCTest是否运行测试。你可以使用测试导航或编辑scheme来禁用测试。你也可以使用测试导航或源代码编辑器中的运行按钮来运行一个测试或一组测试。
对于每个类,运行类中的setup方法开始测试。对于每个测试方法,将分配类的新实例并执行该实例的setup方法。之后,运行测试方法,在最后运行实例的teardown方法。在该类中的所有方法都重复该顺序。在运行类中最后测试方法的teardown,Xcode执行该类的teardown方法并移动到下一个类。重复此顺序知道所有测试类中的测试方法都运行完。
编写测试方法
通过编写测试方法往测试类中添加测试。测试方法是测试类的实例方法,测试方法的前缀是test,没有参数返回void,例如(void)testColorIsRed()
。测试方法测试你项目中的代码,如果该代码不产生预期的结果,断言API会返回失败。例如,一个函数的返回值也许与预期值不一样,或者在一个类中方法使用不当抛出异常。XCTest断言(XCTest Assertions)描述了这些断言。
对于要访问待测试代码的测试方法,将相关头文件导入到你的测试类中。
当在Xcode中运行测试,它独立的调用每个测试方法。因此,每个方法必须准备和清理任何与API交互的辅助变量、结构和对象。如果该代码对类中所有测试方法都通用,你可以将其添加到所需的setup和teardown实例方法中,参见测试类结构( Test Class Structure)中的描述。
下面是单元测试方法的model:
<pre><code>
-(void)testColorIsRed {
// Set up, call test subject API. (Code could be shared in setUp method.)
// Test logic and values, assertions report pass/fail to testing framework.
// Tear down. (Code could be shared in tearDown method.
}
</pre></code>
这里有一个简单的测试方法检测CalcView实例是否创建成功,SampleCalc在快速启动(Quick Start )章节中展示的应用。
<pre><code>
-(void) testCalcView {
// setup
app = [NSApplication sharedApplication];
calcViewController = (CalcViewController*)[NSApplication sharedApplication]
delegate];
calcView = calcViewController.view;
XCTAssertNotNil(calcView, @"Cannot find CalcView instance");
// no teardown needed
}
</pre></code>
编写异步操作测试
测试是同步执行的,因为每个测试都一个一个独立的调用。但很多代码是异步执行。为了处理异步执行方法和函数的测试组件,Xcode6中的XCTest有所改善:等待异步回调完成或超时,序列化异步执行的测试方法。
例子:
<pre><code>
// Test that the document is opened. Because opening is asynchronous,
// use XCTestCase's asynchronous APIs to wait until the document has
// finished opening.
-(void)testDocumentOpening
{
// Create an expectation object.
// This test only has one, but it's possible to wait on multiple expectations.
XCTestExpectation *documentOpenExpectation = [self
expectationWithDescription:@"document open"];
NSURL *URL = [[NSBundle bundleForClass:[self class]]
URLForResource:@"TestDocument" withExtension:@"mydoc"];
UIDocument *doc = [[UIDocument alloc] initWithFileURL:URL];
[doc openWithCompletionHandler:^(BOOL success) {
XCTAssert(success);
// Possibly assert other things here about the document after it has
opened...
// Fulfill the expectation-this will cause -waitForExpectation
// to invoke its completion handler and then return.
[documentOpenExpectation fulfill];
}];
// The test will pause here, running the run loop, until the timeout is hit
// or all expectations are fulfilled.
[self waitForExpectationsWithTimeout:1 handler:^(NSError *error) {
[doc closeWithCompletionHandler:nil];
}];
}
</pre></code>
关于编写异步操作方法的详细信息,参见XCTest.framework
中的XCTestCase+AsynchronousTesting.h
头文件。
编写性能测试
性能测试需要运行十次代码块,收集平均执行时间和标准偏差。这些单独测量的平均值可以与基准时间做比较,评估成功或失败。
注意:基准值是你指定用于评估测试通过或失败的值。用户界面提供设置或更改基准值的机制。
为了实现性能测试,使用Xcode6及更高版本中XCTest的新API编写方法。
<pre><code>
-(void)testPerformanceExample {
// This is an example of a performance test case.
[self measureBlock:^{
// Put the code you want to measure the time of here.
}];
}
</pre></code>
下面例子展示了性能测试,测试计算器示例应用的加法速度。
<pre><code>
-(void) testAdditionPerformance {
[self measureBlock:^{
// set the initial state
[calcViewController press:[calcView viewWithTag: 6]]; // 6
// iterate for 100000 cycles of adding 2
for (int i=0; i<100000; i++) {
[calcViewController press:[calcView viewWithTag:13]]; // +
[calcViewController press:[calcView viewWithTag: 2]]; // 2
[calcViewController press:[calcView viewWithTag:12]]; // =
}
}];
}
</pre></code>
一旦运行性能测试,在测试导航和报表导航中可以查看实现文件。点击信息查看单独的运行值。结果显示以后运行测试的基准值。在每台设备上都会存储基准值,所以你可以在不同的设备上执行相同的测试,基准值会根据特定配置的处理器速度、内存等等有所不同。
注意:性能测试总是在第一次运行时失败直到在特定设备上配置基准值。
关于编写性能测试方法的详情,参见XCTest.framework
中的 XCTestCase.h
头文件。
编写UI测试
使用XCTest创建UI测试与创建单元测试是类似的。类似的操作和类似的变成方法。在流程和实现上的差异主要集中在使用UI录制和XCTest UI测试API,参见用户界面测试(User Interface Testing)。
使用Swift编写测试
Swift访问控制model组织测试访问app或框架中的内部声明。为了在Xcode6中可以访问Swift内部函数,你需要设置测试的入口点为public,减少Swift类型安全的好处。
例如,考虑名为 “MySwiftApp.”的应用Swift模块实现AppDelegate
。
<pre><code>
import Cocoa
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
@IBOutlet weak var window: NSWindow!
func foo() {
println("Hello, World!")
}
}
</pre></code>
编写一个测试类运许访问AppDelegate类,在测试代码中修改import语句,如下:
<pre><code>
// Importing XCTest because of XCTestCase
import XCTest
// Importing AppKit because of NSApplication
import AppKit
// Importing MySwiftApp because of AppDelegate
@testable import MySwiftApp
class MySwiftAppTests: XCTestCase {
func testExample() {
let appDelegate = NSApplication.sharedApplication().delegate as! AppDelegate
appDelegate.foo()
}
}
</pre></code>
有了这个解决方案,测试类和测试方法完全可以访问你的Swift应用代码的内部功能。引入@testable保证其他非测试客户端不违反Swift的访问控制规则,即便是在编译测试。
注意:@testable只为“内部”函数提供访问;当声明为private时在文件外部是不可见的,即便使用@testable。
XCTest断言
你的测试方法使用的断言是由XCTest框架提供的,并在Xcode中显示测试结果。所有断言都有一个类似的格式:比较或逻辑表达式,失败结果字符串格式以及插入到字符串格式中的参数。
注意:所有断言的最后一个参数是format...一个格式字符串和变量参数列表。XCTest提供所有断言默认失败结果字符串。格式字符串提供额外自定义描述失败的功能,此参数是可选的可以省略。
例如,快速入门( Quick Start)中testAddition
方法的断言:
XCTAssertEqualObjects([calcViewController.displayField stringValue], @"8", @"Part 1 failed.");
阅读以下代码,它说“当试图控制器显示的字符串不为参考字符串8,表明失败”。如果断言失败,Xcode在测试导航、源代码编辑器及其他地方显示失败,Xcode中问题导航栏同样会显示失败及描述字符串。源代码编辑器中显示的典型结果如下:
测试方法可以包括多个断言。只有断言失败时,Xcode才显示测试方法失败。
断言分为五类:无条件失败,等于测试,nil测试,布尔测试和异常测试。例如:
无条件失败
XCTFail。无条件的返回失败
XCTFail(format...)
等于测试
XCTAssertEqual(expression1, expression2, format...)
XCTAssertEqual。当表达式1不等于表达式2时返回失败。用于测试标量值。
参见断言( Assertions Listed by Category)了解所有XCTest断言
使用Objective-C和Swift断言
当使用XCTest断言,你应该知道Objective-C(和其他基于C的语言)与Swift断言兼容性和行为的基本不同。了解这些差异是你编写和调试测试变得更容易。
XCTest断言执行等于测试分为对象间的比较和非对象间的比较。例如,XCTAssertEqualObjects
测试一个对象类型的两个表达式是否相等。XCTAssertEqual
测试标量类型的两个表达式是否相等。这种差异在XCTest断言列表中标记为“这个测试时标量”。通过这种方式标记断言为“标量”告诉你基本的区别,但不是一个准确的描述,因为表达式类型是兼容的。
- 对于Objective-C,标量类型的断言可以使用等于比较运算符:
==
,!=
,<=
,<
,>=
,和>。如果表达式解析任何C类型、结构或数组,同样适用,就因为它们也被认为是标量。 - 对于Swift,标记为标量的断言可以用来比较任何符合Equatable协议(所有相等和不相等的断言)和Comparable协议(大于或小于断言)的表达式类型。此外,标记为标量的断言覆盖
[T]
和[K:V],他们均符合Equatable或Comparable协议。例如,数组的等于类型兼容``XCTAssertEqual,字典的键和值都是可比较类型,兼容``XCTAssertLessThan。
- 注意:在Swift中,NSObjest符合Equatable,所以使用
XCTAssertEqualObjects
有效但不是必须的。
在测试中使用XCTest也是不同的,因为在Objective-C和Swift中处理数据类型和隐式转换不同。
- 对于Objective-C,XCTest实现允许隐式转换,允许比较两个相互独立的数据类型表达式,不检查输入数据的类型。
- 对于Swift,隐式转换是不允许的,因为Swift对类型安全更严格,两个比较的参数必须是同一类型的。在编译时会在源代码编辑器标记类型不匹配。
断言
XCTestAssertions分为五组:无条件失败,等于测试,nil测试,布尔测试
下面列出XCTest断言。你可以在快速帮助中XCTestAssertions.h
获取XCTest断言的更多信息。
无条件失败
XCTFail。无条件的返回失败
XCTFail(format...)
等于测试
XCTAssertEqualObjects。当表达式1不等于表达式2(或一个对象是nil另一个不是)时返回失败。
XCTAssertEqualObjects(expression1, expression2, format...)
XCTAssertNotEqualObjects。当表达式1等于表达式2时返回失败。
XCTAssertNotEqualObjects(expression1, expression2, format...)
XCTAssertEqual。当表达式1不等于表达式2时返回失败。用于测试标量值。
XCTAssertEqual(expression1, expression2, format...)
XCTAssertNotEqual。当表达式1等于表达式2时返回失败。用于测试标量值。
XCTAssertNotEqual(expression1, expression2, format...)
XCTAssertEqualWithAccuracy。当表达式1与表达式2之间的差值大于accuracy。这个测试用于标量值如float、double。
XCTAssertEqualWithAccuracy(expression1, expression2, accuracy, format...)
XCTAssertNotEqualWithAccuracy。当表达式1与表达式2之间的差值小于或等于accuracy。这个测试用于标量值如float、double。
XCTAssertNotEqualWithAccuracy(expression1, expression2, accuracy, format...)
XCTAssertGreaterThan。当表达式1小于或等于表达式2时返回失败。用于测试标量值。
XCTAssertGreaterThan(expression1, expression2, format...)
XCTAssertGreaterThanOrEqual。当表达式1小于表达式2时返回失败。用于测试标量值。
XCTAssertGreaterThanOrEqual(expression1, expression2, format...)
XCTAssertLessThan。当表达式1大于或等于表达式2时返回失败。用于测试标量值。
XCTAssertLessThan(expression1, expression2, format...)
XCTAssertLessThanOrEqual。当表达式1大于表达式2时返回失败。用于测试标量值。
XCTAssertLessThanOrEqual(expression1, expression2, format...)
nil测试
XCTAssertNil。当表达式参数不是是nil时返回失败。
XCTAssertNil(expression, format...)
XCTAssertNotNil。当表达式参数是nil时返回失败。
XCTAssertNotNil(expression, format...)
布尔测试
XCTAssertTrue。当表达式计算结果为false时返回失败。
XCTAssertTrue(expression, format...)
XCTAssert。当表达式计算结果为false时返回失败。XCTAssertTrue
的代名词。
XCTAssert(expression, format...)
XCTAssertFalse。当表达式计算结果为true时返回失败。
XCTAssertFalse(expression, format...)
异常测试
XCTAssertThrows。当表达式不抛出异常时返回失败。
XCTAssertThrows(expression, format...)
XCTAssertThrowsSpecific。当表达式不抛出特定类的异常时返回失败。
XCTAssertThrowsSpecific(expression, exception_class, format...)
XCTAssertThrowsSpecificNamed。当表达式不抛出特定类的特定名称的异常则返回失败。AppKit framework或Foundation抛出的异常名字以NSException
开头(NSInvalidArgumentException
等等)。
XCTAssertThrowsSpecificNamed(expression, exception_class, exception_name, format...)
XCTAssertNoThrow。当表达式抛出异常时返回失败。
XCTAssertNoThrow(expression, format...)
XCTAssertNoThrowSpecific。当表达式抛出指定类的异常时返回失败。其他任何异常都通过,即,不返回失败。
XCTAssertNoThrowSpecific(expression, exception_class, format...)
XCTAssertNoThrowSpecificNamed。当表达式抛出特定类的特定名称的异常时返回失败。AppKit framework或Foundation抛出的异常名字以NSException
开头(NSInvalidArgumentException
等等)。
XCTAssertNoThrowSpecificNamed(expression, exception_class, exception_name, format...)