美文网首页
[iOS-Practice] 单元测试

[iOS-Practice] 单元测试

作者: 水止云起 | 来源:发表于2016-12-16 10:34 被阅读63次

    关于单元测试

    在计算机编程中,单元测试(英语: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 中设置。

    相关文章

      网友评论

          本文标题:[iOS-Practice] 单元测试

          本文链接:https://www.haomeiwen.com/subject/lxybmttx.html