iOS单元测试

作者: 大慈大悲大熊猫 | 来源:发表于2016-07-31 11:28 被阅读1085次

    前言

    不写单元测试的程序员是不合格的,为了让自己成为一名合格的程序员,学习如何写单元测试是很有必要的,这里以Xcode集成的测试框架XCTest为例。本文首先会介绍XCTest单元测试的基础用法,然后结合具体的实例分析,最后动手写一个单元测试。

    XCTest

    基础用法

    默认的测试类继承自XCTestCase,当然也可以自定义测试类,添加一些公共的辅助方法。例如AFNetworking的所有测试用例类都有一个共同的父类AFTestCase,它是XCTestCase的子类,AFNetworking所有测试类都是AFTestCase类的子类,这块在后面会具体讲到。需要额外注意的是所有的测试方法都必须以test开头,且不能有参数,不然不会识别为测试方法,具体如下:

    @interface DemoUnitTestsTests : XCTestCase
    
    @end
    
    @implementation DemoUnitTestsTests
    // 在每一个测试用例开始前调用,用来初始化相关数据
    - (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.
    }
    // 性能测试方法,通过测试block中方法执行的时间,比对设定的标准值和偏差觉得是否可以通过测试
    - (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
    
    断言

    XCTest的断言具体可查阅XCTestAssertions.h文件,这里还是做个简单的总结

    //通用断言
    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...) 
    //异常测试,当expression没有发生异常时通过测试;
    XCTAssertNoThrow(expression, format…) 
    //异常测试,当expression没有发生具体异常、具体异常名称的异常时通过测试,反之不通过;
    XCTAssertNoThrowSpecific(expression, specificException, format...) 
    //异常测试,当expression没有发生具体异常、具体异常名称的异常时通过测试,反之不通过
    XCTAssertNoThrowSpecificNamed(expression, specificException, exception_name, format...) 
    

    当然在有些特殊情况下直接使用这些断言,会让代码看起来很臃肿,比如:

    XCTAssertTrue([string isKindOfClass:[NSString class]] && ([[NSUUID alloc] initWithUUIDString:string] != nil), @"'%@' is not a valid UUID string", string);
    

    我们可以自定义断言宏来解决这个问题:

    #define AssertIsValidUUIDString(a1) \ do { \ NSUUID *_u = ([a1 isKindOfClass:[NSString class]] ? [[NSUUID alloc] initWithUUIDString:(a1)] : nil); \ if (_u == nil) { \ XCTFail(@"'%@' is not a valid UUID string", a1); \ } \ } while (0)
    

    使用时只需要调用AssertIsValidUUIDString(string)即可,更多的封装:

    #define assertTrue(expr) XCTAssertTrue((expr), @"")
    
    #define assertFalse(expr) XCTAssertFalse((expr), @"")
    
    #define assertNil(a1) XCTAssertNil((a1), @"")
    
    #define assertNotNil(a1) XCTAssertNotNil((a1), @"")
    
    #define assertEqual(a1, a2) XCTAssertEqual((a1), (a2), @"")
    
    #define assertEqualObjects(a1, a2) XCTAssertEqualObjects((a1), (a2), @"")
    
    #define assertNotEqual(a1, a2) XCTAssertNotEqual((a1), (a2), @"")
    
    #define assertNotEqualObjects(a1, a2) XCTAssertNotEqualObjects((a1), (a2), @"")
    
    #define assertAccuracy(a1, a2, acc) XCTAssertEqualWithAccuracy((a1),(a2),(acc))
    
    期望

    期望实际上是异步测试,当测试异步方法时,因为结果并不是立刻获得,所以我们可以设置一个期望,期望是有时间限定的的,fulfill表示满足期望。
    例如:

    - (void)testAsynExample { 
      XCTestExpectation *exp = [self expectationWithDescription:@"这里可以是操作出错的原因描述。。。"]; 
      NSOperationQueue *queue = [[NSOperationQueue alloc]init]; 
      [queue addOperationWithBlock:^{
     //模拟这个异步操作需要2秒后才能获取结果,比如一个异步网络请求 
      sleep(2); 
    //模拟获取的异步操作后,获取结果,判断异步方法的结果是否正确
       XCTAssertEqual(@"a", @"a"); 
    //如果断言没问题,就调用fulfill宣布测试满足 
      [exp fulfill]; 
      }]; 
    //设置延迟多少秒后,如果没有满足测试条件就报错
     [self waitForExpectationsWithTimeout:3 handler:^(NSError * _Nullable error) {
       if (error) { 
          NSLog(@"Timeout Error: %@", error); 
        }
     }];
    }
    

    异步测试除了使用 expectationWithDescription以外,还可以使用 expectationForPredicateexpectationForNotification,具体的可以看看这里

    实例分析

    这里以AFNetworking为例,前面提到了AFNetworking的所有测试用例类都有一个共同的父类AFTestCase,它也是XCTestCase的子类。在这个类中,添加了一些熟悉和公共方法:

    #import <XCTest/XCTest.h>
    
    extern NSString * const AFNetworkingTestsBaseURLString;
    
    @interface AFTestCase : XCTestCase
    
    /**
     *  默认 https://httpbin.org/ 一个http库测试工具
     */
    @property (nonatomic, strong, readonly) NSURL *baseURL;
    @property (nonatomic, assign) NSTimeInterval networkTimeout;
    
    - (void)waitForExpectationsWithCommonTimeoutUsingHandler:(XCWaitCompletionHandler)handler;
    
    @end
    

    这里有两个属性,一个方法,baseURL不用说是测试地址。networkTimeout是网络请求超时时间,waitForExpectationsWithCommonTimeoutUsingHandler是超时后的方法捕获回调,那么什么时候调用这个方法呢,举个例子:
    在Xcode 6之前的版本里面并没有内置XCTest,想使用异步测试的只能是在主线程的RunLoop里面使用一个while循环,然后一直等待响应或者直到timeout:

    - (void)testAsync {
            NSDate *timeoutDate = [NSDate dateWithTimeIntervalSinceNow:5.0];
            __block BOOL responseHasArrived = NO;
        
            [self requestUrl:@"http://httpbin.com"
                      completionHandler:^(NSString *info) {
                      
                responseHasArrived = YES;
                XCTAssert(info.length > 0);
            }];
        
            while (responseHasArrived == NO && ([timeoutDate timeIntervalSinceNow] > 0)) {
                // 启动runloop,设置RunLoop最大时间(假无限循环),执行完毕是否退出
                CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.01, YES);
            }
        
            if (responseHasArrived == NO) {
                XCTFail(@"Test timed out");
            }
    }
    

    while循环在主线程里面每隔0.01秒会跑一次,直到有响应或者5秒之后超出响应时间限制才会跳出。
    而使用XCTest的测试期望来实现这个,测试框架就会预计它在之后的某一时刻被实现。最终的程序完成代码块中的测试代码会调用XCTestExpection类中的fulfill方法来实现期望。这一方法替代了我们之前例子里面使用responseHasArrived作为Flag的方式,这时我们让测试框架等待(有时限)测试期望通过XCTestCase的waitForExpectationsWithTimeout:handler:方法实现。如果完成处理的代码在指定时限里执行并调用了fulfill方法,那么就说明所有的测试期望在此期间都已经被实现。如:

    - (void)testAsync {
      XCTestExpectation *expectation =
      [self expectationWithDescription:@"High Expectations"];
      [self.pageLoader requestUrl:@"http://httpbin.com"
                      completionHandler:^(NSString *info) {
                      
                XCTAssert(info.length > 0);
                [expectation fulfill];
            }];
        
            [self waitForExpectationsWithTimeout:5.0 handler:^(NSError *error) {
                if (error) {
                    NSLog(@"Timeout Error: %@", error);
                }
            }];
    }
    

    在最后的代码段里面使用[expectation fulfill]来告知此次测试所期望的部分已经确切实现过了。然后用waitForExpectationsWithTimeout:handler方法等待响应,这段会在接受响应之后执行或者超时之后也会执行。

    实战

    还是以AFNetworking为例,写一个测试网络请求的测试用例,这里用cocoapods导入AFNetworking,需要注意的是此时AFNetworking在单元测试里无法使用,需要手动配置路径,步骤为:

    • 1.复制Target(App) - Build Setting - Header Search Paths 的路径。
    • 2.粘贴到Target(UnitTests) - Build Setting - Header - Search Paths里。
    • 3.复制Target(App) - Build Setting - User-Defined - PODS_ROOT整条。
    • 4.到Target(UnitTests) - Build Setting - User-Defined新建一条PODS_ROOT。

    大部分网络请求都是异步操作,但是我们需要在主线程中获取到网络请求成功还是失败的信息。由于测试方法主线程执行完就会结束,所以需要设置一下,查看异步返回结果。这里我们使用期望在方法结束前设置等待,如下:

    -(void)testRequest{
        
        XCTestExpectation *expectation =[self expectationWithDescription:@"没有满足期望"];
        AFHTTPSessionManager *sessionManager = [AFHTTPSessionManager manager];
        sessionManager.responseSerializer = [AFHTTPResponseSerializer serializer];
        [sessionManager GET:@"http://www.weather.com.cn/adat/sk/101110101.html" parameters:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
            
            NSLog(@"responseObject:%@", [NSJSONSerialization JSONObjectWithData:responseObject options:1 error:nil]);
            XCTAssertNotNil(responseObject, @"返回出错");
            [expectation fulfill];
        } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
            XCTAssertNil(error, @"请求出错");
        }];
        // 设置5秒的超时时间
        [self waitForExpectationsWithTimeout:5.0 handler:^(NSError *error) {
            if (error) {
                NSLog(@"Timeout Error: %@", error);
            }
        }];
    }
    

    相关的Demo在这里

    博客地址

    Reference

    XCTest测试实战
    iOS单元测试
    iOS单元测试(作用及入门提升)

    相关文章

      网友评论

      本文标题:iOS单元测试

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