关于单元测试
在计算机编程中,单元测试(英语:Unit Testing)又称为模块测试, 是针对程序模块的最小单位来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。 -- 维基百科
《你应该知道的单元测试》这篇文章对单元测试的基础思想做了很好的总结。
ObjC 中国的期刊在第15期也讨论了“测试”这个专题。
XCTest
XCTest
是苹果公司提供的一个非常简单并且直接集成在 Xcode 中的测试框架。当工程创建时,Xcode 会自动为我们创建一个名为ProjectNameTests
的路径并添加一个测试用例模板文件ProjectNameTests.m
(如果创建时未添加,之后可以通过添加 target 的方式增加测试 bundle)。通过这个模板文件,我们可以了解XCTest
框架的使用方法。
#import <XCTest/XCTest.h>
@interface UnitTestDemoTests : XCTestCase
@end
@implementation UnitTestDemoTests
- (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
首先,我们的测试用例类要继承自XCTestCase
类,其中[setUp]
方法会在每个测试方法前执行,而[tearDown]
方法会在每个测试方法后执行,真正的测试方法必须以 testXXX 的格式命名,且不能有参数。
测试时,快捷键command + u
可以一次执行所有的测试,也可以点击每个测试方法旁的播放按钮执行单独的测试。
实践
那么,我们怎样来写一个测试用例呢?测试用例的意义在于,验证某个类的某个行为在某种上下文中是否能得到预期的结果。通常,我们可以根据 Given-When-Then 模式来组织我们的测试用例,将测试用例拆分成三个部分。
- Given:准备测试功能的上下文,包括测试方法需要的参数等。
- When:执行真正要测试的代码。
- Then:根据功能执行的结果断言测试是否通过。
例:
- (void)testThatItDoesURLEncoding {
// given
NSString *searchQuery = @"$content$amp;?@"; HTTPRequest *request = [HTTPRequest requestWithURL:@"/search?q=%@", searchQuery];
// when
NSString *encodedURL = request.URL;
// then
XCTAssertEqualObjects(encodedURL, @"/search?q=%24%26%3F%40");
}
在 Then 阶段,XCTest
框架提供了多个断言宏供我们使用:
//生成一个失败的测试
XCTFail(format…)
//为空判断,a1为空时通过,反之不通过
XCTAssertNil(a1, format...)
//不为空判断,a1不为空时通过,反之不通过
XCTAssertNotNil(a1, format…)
//当expression求值为TRUE时通过
XCTAssert(expression, format...)
//当expression求值为TRUE时通过
XCTAssertTrue(expression, format...)
//当expression求值为False时通过
XCTAssertFalse(expression, format...)
//判断相等,[a1 isEqual:a2]值为TRUE时通过
XCTAssertEqualObjects(a1, a2, format...)
//判断不等,[a1 isEqual:a2]值为False时通过
XCTAssertNotEqualObjects(a1, a2, format...)
//判断相等(当a1和a2是 C语言标量、结构体或联合体时使用,实际测试发现NSString也可以)
XCTAssertEqual(a1, a2, format...)
//判断不等(当a1和a2是 C语言标量、结构体或联合体时使用)
XCTAssertNotEqual(a1, a2, format...)
//判断相等,(double或float类型)提供一个误差范围,当在误差范围(+/-accuracy)以内相等时通过测试
XCTAssertEqualWithAccuracy(a1, a2, accuracy, format...)
//判断不等,(double或float类型)提供一个误差范围,当在误差范围以内不等时通过测试
XCTAssertNotEqualWithAccuracy(a1, a2, accuracy, format...)
//异常测试,当expression发生异常时通过
XCTAssertThrows(expression, format...)
//异常测试,当expression发生specificException异常时通过;反之发生其他异常或不发生异常均不通过
XCTAssertThrowsSpecific(expression, specificException, format...)
//异常测试,当expression发生具体异常、具体异常名称的异常时通过测试,反之不通过
XCTAssertThrowsSpecificNamed(expression, specificException, exception_name, format...)
XCTAssertNoThrow(expression, format…) //异常测试,当expression没有发生异常时通过测试;
//异常测试,当expression没有发生具体异常、具体异常名称的异常时通过测试,反之不通过
XCTAssertNoThrowSpecific(expression, specificException, format...)
//异常测试,当expression没有发生具体异常、具体异常名称的异常时通过测试,反之不通过
XCTAssertNoThrowSpecificNamed(expression, specificException, exception_name, format...)
另外,如果多个测试用例类需要一些相同的初始化条件,我们可以实现一个XCTestCase
类的派生类作为基类。在这个类中实现一些公共的方法和属性。之后,测试用例类直接继承自这个基类,要注意在[setUp]
方法和[tearDown]
方法中调用 super 的实现。
网络请求的测试
由于单元测试是在主线程中进行的,因此如果只是在网络请求异步响应的方法中执行断言,那么测试在异步操作返回结果前就已经结束了,无法达到测试的目的。对于异步操作的测试,XCTest
框架提供了这样一种机制,首先在测试方法中关联一个代表期望的XCTestExpectation
实例,然后在测试方法结束前执行方法[- waitForExpectationsWithTimeout:handler:]
,该方法会执行一个 run loop 直到所有的期望实例执行了方法[- fulfill]
(即期望达成)或者达到超时时间。例如 AFNetworking 中的一个测试:
- (void)testDataTaskDoesReportDownloadProgress {
NSURLSessionDataTask *task;
__weak XCTestExpectation *expectation = [self expectationWithDescription:@"Progress should equal 1.0"];
task = [self.localManager
dataTaskWithRequest:[self bigImageURLRequest]
uploadProgress:nil
downloadProgress:^(NSProgress * _Nonnull downloadProgress) {
if (downloadProgress.fractionCompleted == 1.0) {
[expectation fulfill];
}
}
completionHandler:nil];
[task resume];
[self waitForExpectationsWithCommonTimeoutUsingHandler:nil];
}
通过上述机制,虽然我们可以直接测试真正的网络请求,但是真实网络环境是非常复杂的,返回的响应具有不确定性,为了达到单元测试关注点单一的目的,我们可能需要模拟确定的网络请求响应数据。OHHTTPStubs 通过NSURLProtocol
实现了模拟网络请求响应的功能,是在对网络请求相关代码进行单元测试时,非常好用的工具。这篇文章对其实现原理进行了介绍:《如何进行 HTTP Mock(iOS)》
如果是通过 Cocoapods 来安装管理 OHHTTPStubs 的话,那么默认在 test target 中 import 头是找不到 pods 中的类库的,需要在 test target 的 Build Settings 中设置 Header Search Paths,可以复制产品 target 中对应的值,而其中的 pods 路径别名也需要在 test target 中设置。