iOS单元测试

前言

不写单元测试的程序员是不合格的,为了让自己成为一名合格的程序员,学习如何写单元测试是很有必要的,这里以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单元测试(作用及入门提升)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,047评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,807评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,501评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,839评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,951评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,117评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,188评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,929评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,372评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,679评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,837评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,536评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,168评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,886评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,129评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,665评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,739评论 2 351

推荐阅读更多精彩内容