原文地址:《Writing Test Class and Methods》

文章导读

本篇介绍了如何编写测试类和测试方法。

看完之后你能学习到:

声明

文章翻译自 Apple 官方文档《Testing with Xcode》,不保证每个字都能翻译的精准,如有翻译错误,请留言指出,不胜感激。

Writing Test Classes and Methods

当你通过测试导航菜单为一个项目添加测试时,Xcode 会在测试导航菜单中展示测试类和测试方法。在测试目标中,测试类包含着测试方法,这章将想你解释如何创建测试类和编写测试方法。

在看创建测试类之前,先看看测试导航菜单更有价值。使用它是创建测试和使用测试的核心。

为一个项目增加测试目标,创建一个测试包。测试导航菜单列出了项目源代码中所有测试报。用层级列表的方式展示测试类和测试方法,这里展示了一个项目中两个测试目标的测试导航菜单视图,显示了一套层级列表形式的测试包,测试类和测试方法。

twx-wtcm-1_2x

测试包能容纳许多测试类。无论是功能还是有组织目的的归类,你都能用测试类来区分不同的测试组。举个例子,对于计算的示例项目,你可以创建BasicFunctionsTestsAdvancedFunctionsTestsDisplayTests类,所有类都在Mac_Calc_Tests包里。

twx-wtcm-2_2x

某些类型的测试可能需要共享sepUptesrDown方法,把这些测试方法放到一个类是明智的。一个单独的setUptestDown方法可以减少你为所有测试方法写它们的工作量。

Creating a Test Class


注意: 本章通过举例的目的来聚焦单元测试的测试类和测试方法,创建 UI 测试目标,类和方法。它们和单元测试的区别,在User Interface Testing讨论。


你可以使用测试导航菜单中的增加按钮(+)来创建新的测试类。

twx-wtcm-3_2x

你可以选择增加单元测试类或者 UI 测试类,选择其中一个之后,Xcode 会展示一个模板目录选择的界面。下图中高亮显示了一个单元测试类的模板。点击Next确认你的选择。

twx-wtcm-4_2x
项目中每一个测试类中增加一个名叫TestClassName.m。基于在配置中你输入的测试类的名称。

twx-wtcm-5_2x


注意:所有的测试类都继承于由 XCTest 框架提供的XCTestCase


虽然 Xcode 的默认的组件会把创建的测试放在你创建测试目标的目录下,但是你也可以放在项目中任意一个你选择的位置。当你选择下一步的时候,标准的 Xcode 新增目录的配置图入如下所示。

twx-wtcm-6_2x

当你在项目导航中新增一个新目录时,方式也是一样的,更多有关Add Files sheet的内容,请看Adding an Existing File or Folder


注意:当你新建一个项目的时候,测试目标和测试包会默认的基于你的项目名称而命名。在这种情况下,创建一个名称为MyApp的时候,自行创建一个名为MyAppTests的测试包和一个名为MyAppTests测试类,以及一个名为MyAppTests.m的可执行文件。


Test Class Structure

测试类有这样的基本结构:

#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

这个测试类的例子,不仅可以在 Object-C 中运行,也可以在 Swift 下运行。


注意:为了保证统一性,本文所有的例子都是用 Object-C 编写。
XCTest 也全部支持使用 Swift 编写你的测试方法。所有的 Object-C 和 Swift 混编的方法也同样支持。


需要注意的是,基础执行方法中包含该了setUptearDown方法,这些方法不是必须的。如果某个类中的所有方法都需要相同的代码。你在你的类中包含这两个方法,并对它们做一些改造。这些代码在每一条测试用例前后都会执行一遍。你可以为测试类增加同样的类方法,setUp(+(void)setUp)tearDown((+void)tearDown)。它们会在测试类的所有方法执行前和执行后执行。

Flow of Test Execution

在默认的例子中,当我们执行测试,XCTest 会找到所有的测试类,并执行它们下面的所有测试方法(所有的测试类继承于XCTestCase)。


注意:在 XCTest 运行时,一些配置允许你对测试做一些特别的更改。你可以在测试导航器中不执行某些测试或者编辑测试计划。你也可以在测试导航器中选择只执行一条测试用例或者一组测试用例或者在代码编辑器中直接编辑。


对于每个类来说,从每个类的setUp方法开始执行测试。对于每个测试方法来说,一个类被实例化,就会先执行setUp方法,然后执行测试方法,最后执行tearDown方法。这一系列动作在每个测试方法中重复。在最后一个tearDown方法被执行后,测试类就被执行完毕了。Xcode 执行完tearDown类方法后就开始执行下一个测试类。这一系列动作一致重复到所有的测试类都被执行完。

Writing Test Methods

你增加测试用例的方式是为测试类增加测试方法。一个测试方法是测试类一种情况,使用test作为前缀,没有参数,返回void。例如(void)testColorIsRed()。在你项目中的一段测试代码,如果它没有产生预期的结果,报告会用一组断言 API 来报告失败。例如一个功能返回内容和你预期的内容冲突了或者你的测试可能会断言在一个类中的测试方法不恰当的抛出了一个异常。XCTest Assertions描述了这些断言。

对于要测试的代码,请将被测试代码的头文件导入到你的测试类中。

当 Xcode 运行测试时,它会独立的运行每一条测试方法。因此,每一种方法必须准备和清楚所有的辅助变量,结构和对象。它需要和主体的 API 进行交互。如果这些代码在类中的所有方法都会用到,你可以像Test Class Structure章节中描述的那样把它们加大setUptearDown方法中。

下面是一个单元测试方法的模板:

- (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.
}

这里有一个简单的测试方法用来检查CalcView是否成功的被 SampleCalc 创建,app 在Quick Start章节中展示了。

- (void) testCalcView {
   // setup
   app = [NSApplication sharedApplication];
   calcViewController = (CalcViewController*)[NSApplication sharedApplication] delegate];
   calcView             = calcViewController.view;

   XCTAssertNotNil(calcView, @"Cannot find CalcView instance");
   // no teardown needed
}

Writing Tests of Asynchronous Operations

测试是同步执行的,因为每一个测试用例都是相互独立的一条接一条的执行下去。但是越来越多的代码是异步执行的。要处理调用异步执行方法和函数的测试组件,从 Xcode6 开始,XCTest 增加了连续异步执行测试用例的能力。通过等待异步回调完成或者超时。

一个源码的例子:

// 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];
    }];
}

更多关于编写异步操作的详细信息,请看XCTest.framework的头文件XCTestCase+AsynchronousTesting.h

Writing Performance Tests

性能测试需要你评估并运行代码十次,手机执行的平均耗时和标准偏移量。这些单独的测量的平均值,形成一个测试运行的值,可以与基线相比以评估成功或失败。


注意:基线是已经被你用来评估成功或者失败的值。报告界面提供了一个设置或更改基线值的途径


执行性能测试,你需要使用 Xcode6 或者以后版本提供的 XCTest 的新的 API。

- (void)testPerformanceExample {
    // This is an example of a performance test case.
    [self measureBlock:^{
        // Put the code you want to measure the time of here.
    }];
}

下面简单的例子展示了使用样例计算器 APP 编写性能测试来测试加法速度的场景。measureBlock是一个加法,随着一个 XCTest 的次数的迭代。

- (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]];  // =
        }
    }];
}

性能测试运行一次,当查看执行文件时,会在代码编辑器上提供信息、也会在测试导航器和报告导航器上展示信息。点击信息会呈现单独的运行结果。测试结果显示包括设置测试结果作为未来测试标准的基线。基线存储在每个设备的配置文件中所以你如果在不同的设备上运行同样的测试用例,由于每个设备处理速度,内存等配置不同会导致出现不同的基线。


注意:性能测试第一次运行总是会报告失败,直到基线在设备的配置文件中被设置。


更多关于性能测试的方法,请查看XCTest.framework的头文件XCTestCase.h

Writing UI Tests

用 XCTest 创建 UI 测试时创建单元测试的一个模型的扩展。类似的操作和程序的模型是一样的。工作流程中的区别是实施 UI 测试使用的是User Interface Testing描述的 XCTest UI 记录工具和 XCTest UI 测试 Api。

Writing Tests with Swift

Swift 存取控制模型阻止测试从 app 或者框架内部声明。在 Xcode6 中使用 Swift 来使用内部功能,你需要为测试设置这些入口点为公共的,降低 Swift 类型安全的好处。

Xcode7 针对这个问题提供了两种解决方案:

  1. Swift 在构建编译的时候直接做成内部可访问的。改变编译的方式是在 Xcode 执行你测试构建的动作时发出一个新的-enable-testing标志。这个行为是由构建设置的Enable Testability来控制的,为新的项目设置默认为Yes。这样你不需要改变你的源代码。
  2. 可测试的入口点的可见性受限于请求它修改导入的语句。修改的实施是通过导入一个新的@testable属性,为你的测试代码做一次调整,app 的代码不需要变动。

例如,为一个名叫 “MySwiftApp” 的 app 考虑 Swift 的模型像AppDelegate这样的方式。

import Cocoa
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
    @IBOutlet weak var window: NSWindow!
    func foo() {
        println("Hello, World!")
    }
}

编写测试类允许AppDelegate类作为入口,你需要在你的测试代码中为@testable属性修改import语句,像下面这样:

// 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()
    }
}

使用这种解决方案,你的 Swift 应用代码内部的功能就可以全部被你的测试类和测试方法访问。准许@testable的导入确认其他非测试客户端没有权限访问 Swift 控制的规则,即使是在编译测试的时候。


注意:@testable提供访问仅仅是内部功能,当使用@testalbe时,私有声明的目录也是无法访问的。


XCTest Assertions

你的测试方法使用 XCTest 框架提供的断言来展示测试结果在 Xcode 上。所有的断言都有一个类似的格式,比对逻辑表达式的项目,一个失败结果的字符串格式和插入到字符串格式中的参数。


注意:所有断言的最后偶一个参数被格式化。一个格式化的字符串和变量参数列表。XCTest 为所有的断言提供了一个默认失败结果的字符串,手机参数来通过断言,format字符串提供了一个情况


例如,查看Quick Start中的testAddition这个断言:

XCTAssertEqualObjects([calcViewController.displayField stringValue], @"8", @"Part 1 failed.");

像自然语言一样读它,是这样的Indicate a failure when a string created from the value of the controller’s display field is not the same as the reference string ‘8’.”。如果断言失败,在测试导航器上,Xcode 会单独展示一个失败的标志,Xcode 也会在问题导航器上或者代码编辑器或者其他地方展示一个失败的描述。在源码编辑器上一个典型的结果是这样的:

一个测试方法可以包括多个断言,任意一个断言报告了失败,Xcode 会标记这个测试方法为失败。

断言失败有五个种类,一定失败,相等测试,为空测试,真值测试和预期测试,例如:

Unconditional Fail

XCTFail,一定产生失败

XCTFail(format...)

Equality Tests

XCTAssertEqual, 当表达式 1 和表达式 2 不相等时给出失败。

XCTAssertEqual(expression1, expression2, format...)

Assertions Listed by Category获取所有 XCTest 的断言。

Using Assertions with Objective-C and Swift

当使用 XCTest 断言的时候,你需要知道断言 Swift 代码和断言 Object-C(其他类 C 语言)代码的基本的不同点。了解这些不同点让你编写和 debug 你的测试更简单。

XCTest 断言做相等测试的时候区分对象比较和非对象比较。例如,XCTAssertEqualObjects测试两个表达式对象类型是否相等。而XCTAssertEqual测试两个表达式的值是否相等。这个差异是标记 XCTest 断言清单包括测试标量的描述??。用标量的方式来通知你断言的基本区别,但是它不能精确的描述表达式不匹配。


注意:在 Swift 中,NSObject遵从Equatable,所以使用XCTAssertEqualObjects也是可以工作的,但是这样不是必须的。


Object——C 和 Swift 在测试中使用 XCTest 断言也是不同的是因为语言处理数据类型和内部转换的方式不同。

Assertions Listed by Category

XCTestAssertions 被分类为五组,无条件失败断言,相等测试,为空测试,真值测试和预期测试。

下面部分是 XCTest 断言的清单。你可以在 Xcode 使用快速帮助查看XCTestAssertions.h了解更多关于 XCTest 断言的信息。

Unconditional Fail

XCTFail,直接产生一个失败

XCTFail(format...)

Equality Tests

XCTAssertEqualObjects,当表达式 1 不等于表达式 2(或者一个对象是空,另一个不是)产生失败

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 大于精确度时产生一个错误,这个标量测试一般针对浮点和双精度,微小的不同之处可以让这些项目他们精确的不相等,但是对所有标量有效。

XCTAssertEqualWithAccuracy(expression1, expression2, accuracy, format...)

XCTAssertNotEqualWithAccuracy,当表达式 1 和表达式 2 小于精确度时产生一个错误,这个标量测试一般针对浮点和双精度,微小的不同之处可以让这些项目他们精确的不相等,但是对所有标量有效。

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 Tests

XCTAssertNil,当表达式参数不为空时产生一个错误。

XCTAssertNil(expression, format...)

XCTAssertNotNil,当表达式参数为空时产生一个错误。

Boolean Tests

XCTAssertTrue,当表达式为false时产生一个错误。

XCTAssertTrue(expression, format...)

XCTAssert,当表达式为false时产生一个错误,与XCTAssertTrue一样。

XCTAssert(expression, format...)

XCTAssertFalse,当表达式为true时产生一个错误。

XCTAssertFalse(expression, format...)

Exception Tests

XCTAssertThrows,当表达式没有抛出异常时产生一个错误。

XCTAssertThrows(expression, format...)

XCTAssertThrowsSpecific,当表达式没有抛出一个特定的类时产生一个错误。

XCTAssertThrowsSpecific(expression, exception_class, format...)

XCTAssertThrowsSpecificNamed,当表达式没有抛出一个特殊的类和一个特殊的名称时产生一个错误。对于那些像 AppKit 和 Foundation 的框架个很有用,抛出一个普通的带有特殊名称(NSInvalidArgumentException等等)的NSException

XCTAssertThrowsSpecificNamed(expression, exception_class, exception_name, format...)

XCTAssertNoThrow,当表达式抛出异常时产生一个错误。

XCTAssertNoThrow(expression, format...)

XCTAssertNoThrowSpecific.当表达式抛出一个特定的类时产生一个错误。任意一个其他异常事通过的。这就意味着,不产生一个错误。

XCTAssertNoThrowSpecific(expression, exception_class, format...)

XCTAssertNoThrowSpecificNamed,当表达式抛出一个特殊的类和一个特殊的名称时产生一个错误。对于那些像 AppKit 和 Foundation 的框架个很有用,抛出一个普通的带有特殊名称(NSInvalidArgumentException等等)的NSException

XCTAssertNoThrowSpecificNamed(expression, exception_class, exception_name, format...)

传送门

前两章已经找朋友校验了一下,改善了一些不友好的描述,已更新。

[译]《Testing with Xcode》第一章——QuickStart

[译]《Testing with Xcode》第二章——Testing Basics


↙↙↙阅读原文可查看相关链接,并与作者交流