测试时从何处开始
当我们开始测试时,应当遵守以下原则:
- 当创建单元测试时,应当着重于代码中最基础的部分,即与
Controller
交互的Model
类和方法。 - 当创建UI测试时,首先考虑最常见的工作流程。想象下用户开始使用app时会做些什么以及过程中执行了哪些UI。使用UI recording 功能可以捕捉用户的一系列动作到测试方法中,也可以扩展该方法来验证测试的正确性或性能。
创建一个测试类
我们可以使用导航栏下方的加号按钮创建一个新的测试类
也可以使用command
+ N
的快捷键方式来创建
上图选择的分别的UI测试和单元测试。
注意: 所有的测试类都是
XCTest
框架的XCTestCase
的子类
测试类的结构
测试类的结构如下:
#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
该实现包含setup
和teardown
实例方法的基本实现,但这些方法并不是必需的。如果所有的测试方法使用了相同的代码,我们可以把这些代码放在setup
和teardown
中。
一般在setup
方法中,进行初始化操作、代码复用和准备测试条件。
在tearDown
方法中,一般会进行释放对象,以避免干扰和回收资源。
测试执行的顺序
默认情况下,当运行测试时,XCTest
会查询所有的测试类,并执行每个测试类中所有的测试方法。
对于每个类,都是以setup
类方法开始的。对于每个测试方法,都会创建一个新的类的实例并执行setup
实例方法。然后执行测试方法,再然后执行teardown
实例方法。类中每个测试方法都是如此。当类中最后一个测试方法的teardown
方法运行之后,Xcode会执行teardown
类方法并移到下一个类中。重复该动作直到所有的测试类都运行完毕。
测试方法的书写
测试方法是一个以test
开头,无参无返回的实例方法。如果测试方法中没有达到我们期望的效果,可以使用一组断言来报告失败。
当 Xcode 运行测试时,它会独立调用每个测试方法。因此,每个方法必须准备和清除任何它需要与主题API交互的辅助变量,结构体和对象。如果类中所有的测试方法都有共有的代码,可以把这些代码添加到setup
和tearDown
实例方法中。
下面就是一个单元测试方法:
- (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.
}
运行测试方法
测试方法的运行有多种类型
- 将鼠标放在导航栏上,也就是图中 1的位置,将会运行bundle 中所有的测试方法
- 将鼠标放在类中的运行按钮处,即图中 2的位置,将运行类中所有的测试方法
- 将鼠标放在方法右方的运行按钮处,即 图中 3位置处,将只运行该测试方法
或者在类文件中也可以,如下图:
测试方法通过将会显示图中的绿色标记,不通过将会出现红色图标,如图所示:
如果要运行工程中所有的测试方法,可以选中 Product > Test
下面的按钮会只显示测试失败的方法
异步操作测试
测试是同步执行的,因为每个测试方法都是独立调用的。但越来越多的代码执行是异步的。为了处理调用异步执行方法的组件和函数,XCTest在Xcode 6中进行了加强,包括通过等待异步回调或超时完成之后,在测试方法中序列化异步执行的
文档中提供的例子:
// 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.
// 创建一个expectation 对象 这里只创建了一个,但是也可以创建多个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.
// 实现该expectation后会调用 -waitForExpectation 方法的 handler block
[documentOpenExpectation fulfill];
}];
// The test will pause here, running the run loop, until the timeout is hit
// or all expectations are fulfilled.
// 测试方法将在这暂停, 运行 runloop, 直到 超过`timeout`时间或者所有的 expectation 都实现了
[self waitForExpectationsWithTimeout:1 handler:^(NSError *error) {
[doc closeWithCompletionHandler:nil];
}];
}
上面代码就是在文档打开之后(调用了fulfill
方法)或者超过 1s(timeout
值)还没打开的话会执行 handler块内的内容。
XCTestExpectation
代表了异步测试中的特定条件。- fulfill
方法标记该期望被满足。
- (void)waitForExpectationsWithTimeout:(NSTimeInterval)timeout handler:(nullable XCWaitCompletionHandler)handler
该方法会在expectation
满足或者在timeout
时间后执行handler
块。
重点:
该方法仅等待
XCTestCase
的便利方法创建的expectation。不会等待XCTestExpectation
或其子类上的初始化程序手动创建的expectation值。(这里我们创建expectation时的self是指XCTestCase
实例,所以会等待)要等待手动创建的expectation,使用
waitForExpectations:timeout:
或waitForExpectations:timeout:enforceOrder:
方法或XCTWaiter上的相应方法,传递一个明确的expectation列表。
性能测试的书写
性能测试会运行想要评估的代码块十次,收集平均执行时间和运行的标准偏差。然后平均值与baseLine
进行比较以评估成功或失败。
baseLine
是我们指定的用来评估测试通过或者失败的值。我们也可以自己指定一个特定的值。
要实现性能测试,我们可以使用Xcode 6之后的新的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.
// 把想要测试的代码放在这里
}];
}
下面的简单示例显示了一个性能测试,用于测试计算器示例应用程序的速度
- (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]]; // =
}
}];
}
UI 测试
UI测试能够查找应用程序的UI并与其进行交互,以验证属性和UI元素的状态。
UI测试包括UI recording,能够生成代码,这些代码可以像用户一样执行应用程序的用户界面,并且可以扩展实现UI测试。这是快速开始编写UI测试的好方法。
UI测试在基本原理方面与单元测试不同。单元测试允许你在app范围内工作,并允许我们通过完全访问应用程序的变量和状态来执行函数和方法。UI测试是以用户相同的方式来操作app的UI,(不会访问app内的方法函数和变量)。这使测试能够以与用户相同的方式查看app,从而暴露用户遇到的UI问题。
APIs
UI测试给予下面三个类的实现:
XCUIApplication
XCUIElement
XCUIElementQuery
UI recording
UI recording 会在测试方法中生成代码,可以对其进行编辑以构建测试或回放特定的使用场景。UI recording 对探索新的UI或学习如何编写UI测试序列也很有用。操作的基本顺序是:
把鼠标放到方法里
-
点击红色录制按钮,开始记录UI。应用程序会启动,这是我们可以点击UI元素(模拟用户行为),鼠标处就会自动生成代码
-
完成录制之后,点击停止录制按钮
可以加入我们自己想要的代码逻辑,比如加入XCTest断言
UI测试正确性的一般模式是:
- 使用 XCUIElementQuery 查找 XCUIElement
- 合成一个事件并把它发送给XCUIElement
- 使用断言来比较 XCUIElement 的状态和预期的参考状态
比如,
- (void)testExample {
// Use recording to get started writing UI tests.
// Use XCTAssert and related functions to verify your tests produce the correct results.
XCUIApplication *app = [[XCUIApplication alloc] init];
XCUIElement *loginElement = [app.otherElements containingType:XCUIElementTypeButton identifier:@"login"].element;
[loginElement twoFingerTap];
XCUIElement *textField = [[app.otherElements containingType:XCUIElementTypeButton identifier:@"login"] childrenMatchingType:XCUIElementTypeTextField].element;
[textField tap];
[textField tap];
[textField tap];
[textField tap];
[textField typeText:@"1234"];
[textField typeText:@"5678"];
[textField typeText:@"9010"];
[textField typeText:@"5201"];
}
运行之后:
会自动执行方法里的内容,执行里面的操作(输入文字和点击按钮)
XCTest 断言
断言分为五种类型
Unconditional Fail (无条件失败). 当到达特定的代码分支指示失败时使用该断言。这个类型中只有一个断言:XCTFail.
Equality Tests. 断言两个item之间的关系。比如,XCTAssertEqual 声明两个表达式具有相同的值,同时XCTAssertEqualWithAccuracy声明两个表达式在一定的准确度内具有相同的值。此类别还包括不等测试,例如XCTAssertNotEqual 和 XCTAssertGreaterThan。
Boolean Tests. 断言布尔表达式,比如 XCTAssertTrue 或者 XCTAssertFalse。
Nil Tests. 断言一个item是否为nil. 比如 XCTAssertNil 或 XCTAssertNotNil 。
Exception Tests (异常测试). 断言一个表达式是否会生成异常,使用 XCTAssertThrows 来抛出异常,也可以使用XCTAssertThrowsSpecific 指定特定的异常,也可以使用 XCTAssertNoThrow 来断言表达式没有异常。
代码覆盖率
代码覆盖率是Xcode 7中的新特性,允许我们可视化并测量代码的执行的程度。通过代码覆盖率,可以确定测试是否正在我们想要的工作。
启用代码覆盖
代码覆盖是LLVM支持的测试选项。当启用代码覆盖,LLVM会根据方法和函数调用的频率来检测代码以收集覆盖率数据。代码覆盖选项可以收集数据以报告测试的正确性和性能,无论是单元测试还是UI测试。
- 选中 edit scheme选项(除了下图的方式,还可以通过
Product -> Scheme -> Edit Scheme
)
- 选中
Test -> Options -> Code Converage
选项
注意: 代码覆盖数据收集会导致性能损失。 无论该损失是否重要,它都会以线性方式影响代码的执行,所以当启用时,性能结果在测试运行之间保持可比性。但是,当你严格评估性能时需要考虑是否启用代码覆盖
代码覆盖如何适用于测试
代码覆盖率是一个衡量测试价值的工具。它回答了以下问题:
- 当你运行测试的时候实际运行的代码是什么?
- 有多少测试才足够?
换句话说,你是否构建了足够多的测试来确保所有的代码都得到了正确性和性能的检查? - 代码中的那些部分没有被测试到?
测试运行完成后,Xcode 将获取LLVM覆盖率数据,并使用它创建一个覆盖率报告。该报告展示了测试运行的主要信息、原文件的列表、文件中的函数以及 每个文件的覆盖百分比
按下图所示,可以查看代码覆盖率:
下面是AFNetworking 的代码覆盖率截图
点击现实的按钮或者双击,可以跳转到源代码
比如我们跳转到了AFAutoPurgingImageCacheTests
文件的testThatImagesArePurgedWhenCapcityIsReached
方法。右侧显示的是覆盖范围的注释,显示了测试过程中特定代码的执行次数。
比如上图的 最上方的 1 指的是该方法执行了一次, 数字11 指的是while循环执行了11次,其他数字同理。
同样的,如果某个方法没有被执行,它的数字就是 0了,
(这里我是单独运行了testThatImagesArePurgedWhenCapcityIsReached
方法,所以上图所示的方法当然是运行0次了)
最后
Xcode对测试的集成支持使您能够编写测试以各种方式支持您的开发工作。您我们可以使用测试来检测代码中的潜在问题,发现是否符合预期,并验证应用程序的行为,提高代码的稳定性。
当然,通过测试获得的稳定水平取决于编写的测试代码的质量。同样,编写好测试的难易程度取决于编写代码的方式。阅读以下指导原则以确保您的代码是可测试的,并且可以简化编写良好测试的过程。
定义API的要求. 定义添加到项目中的每种方法或功能的需求和结果非常重要。对于需求,包括输入和输出范围,抛出的异常和引发它们的条件以及返回值的类型。指定需求并确定代码中需求已经达到可以使我们的代码更加安全强壮。
编写代码时编写测试用例. 在设计和编写每个方法和函数时,编写一个或多个测试用例以确保符合API的需求。为现有代码编写测试比在编写代码时就写测试更困难
检查边界条件. 如果方法的参数必须具有特定范围内的值,则测试应传递包含范围的最低和最高值的值。
使用negative test(负面测试). negative test 可确保我们的代码适当地响应错误条件。验证我们的代码在接收到无效或意外的输入值时行为正确。还应验证它是否返回错误代码或引发异常。例如,如果一个整数参数必须是在0到100范围内,测试用例值传递-1和101以确保该过程引发一个异常或返回错误代码。
大家也可以查看苹果的 官方文档