[iOS-Practice] 单元测试

关于单元测试

在计算机编程中,单元测试(英语:Unit Testing)又称为模块测试, 是针对程序模块的最小单位来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。 -- 维基百科

《你应该知道的单元测试》这篇文章对单元测试的基础思想做了很好的总结。
ObjC 中国的期刊在第15期也讨论了“测试”这个专题。

XCTest

XCTest是苹果公司提供的一个非常简单并且直接集成在 Xcode 中的测试框架。当工程创建时,Xcode 会自动为我们创建一个名为ProjectNameTests的路径并添加一个测试用例模板文件ProjectNameTests.m(如果创建时未添加,之后可以通过添加 target 的方式增加测试 bundle)。通过这个模板文件,我们可以了解XCTest框架的使用方法。

#import 

@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 中设置。

[iOS-Practice] 单元测试_第1张图片

你可能感兴趣的:([iOS-Practice] 单元测试)