iOS单元测试-XCTest

goddess.JPG

前言

单元测试简单来说,就是为了方便测试一些功能是否正常运行,调试接口是否能正常使用,用代码去检测代码是否正确的一种手段。例如:你为了测试某一个网络接口,每次都重新启动,经过很多操作之后,才测试到那个网络接口。如果使用了单元测试,就可以直接测试那个方法,相对方便很多。单元测试不仅没有降低我们Coding的效率,也能保证在之后的改动中及时发现可能出现的错误。

学习单元测试之前,让我们先来看看一些常用第三方所选用的测试框架:

图1.jpg

从图中得知,苹果官方的测试框架XCTest 还是很受欢迎的哈 ~

并不是所有的方法都需要测试,一般而言,私有方法不需要测试,只有暴露在 .h 中的方法需要测试。那到底测试用例的覆盖率是多少才合适呐?其实一个软件覆盖度在50%以上就可以称为一个健壮的软件了,要达到70,80这些已经是非常难了,但“史莱克从来不缺少天才”,例如:AFNNetWorking的覆盖率高达88%,SDWebImage的覆盖率也达到75%。

图2.png
图3.png

一、集成

  1. 创建工程的时候,直接勾选 Include Unit Tests
屏幕快照_2018-08-01_下午6_33_49.png

2.如果已有项目未勾选,则通过以下方式再创建一个
File-->new-->Target-->iOS-->iOS Unit Testing Bundle

图4.png

3.工程创建好之后,找到系统单元测试Tests 文件夹,在 .m文件中就可以写我们的测试用例了,是不是很简单呐~

图5.png

4.一般我们会新建不同的测试用例类与代码类一一对应,可以通过新建 Unit Test Case Class 来实现

图6.png

二、方法

测试用例类 .m 文件中,会有几个默认方法,我们来看下这几个方法是什么时候调用和他们的作用:

- (void)setUp {
    [super setUp];
    //初始化,在测试方法调用之前调用
}

- (void)tearDown {
    // 释放测试用例的资源代码,这个方法会每个测试用例执行后调用
    [super tearDown];
}

- (void)testExample {
    // 测试用例的例子,注意测试用例一定要test开头
}

- (void)testPerformanceExample {
    [self measureBlock:^{
        // 需要测试性能的代码
    }];
}

注意:测试用例必须以Test开头,且不能有参数,不然不会被识别。

三、使用

  1. 快捷键 Command + U 会运行全部单元测试;
  2. 鼠标放在方法右边,会出现播放按钮,点击后开始单个方法的测试;
图7.png
  1. 鼠标放在方法左边,会出现播放按钮,点击后开始单个方法的测试;


    图8.png
  2. 如测试通过,会有“Test Succeeded”提示,且函数左边菱形图标展示为绿色;如测试不通过,会有“Test Failed”提示,且函数左边菱形图标展示为红色。

图9.png

四、测试

1. 基本断言的逻辑测试,关于断言会在文末说明;

例1:有一个函数目的是生成在[base, end]之间的随机数,我们来检测一下会不会出现越界的情况:

// 生成在[base, end]之间的随机数
- (int)randomNumberFrom: (int)base End: (int)top{
    if (base >= top) {
        return base;
    }
    return (arc4random() % (top - base + 1)) + base;
}
- (void)testRandom{
    int base = 3;
    int top = 80;
    
    for (int i=0; i<100; i++) {
        int temp = [self randomNumberFrom:base End:top];
        if (temp < base || temp > top) {
            XCTFail(@"invalid num = %d",temp);
        }
    }
}

例2:在ViewController中写一个简单的方法

- (int)getNum{
    return 100;
}

在测试的文件中导入ViewController.h,并且定义一个vc属性

#import <XCTest/XCTest.h>
#import "ViewController.h"

@interface MJViewControllerTest : XCTestCase
@property (nonatomic, strong) ViewController *VC;
@end

@implementation MJViewControllerTest

测试用例的实现

- (void)setUp {
    [super setUp];
    self.VC = [[ViewController alloc]init];
}

- (void)tearDown {
    self.VC = nil;
    [super tearDown];
}

- (void)testGetNum{
    int result = [self.VC getNum];
    XCTAssertEqual(result, 100, @"不相等,测试不通过");
}

运行测试用例,可以看到测试通过,菱形图标显示绿色。
如果这时我们改下断言,把100随便改成一个数,则测试不通过,如下:

图10.png
2. 异步测试

代码中会有很多异步的场景需要验证,例如网络请求callback中执行的操作,由于测试方法主线程执行完就会结束,所以需要在方法结束前设置等待,调回回来的时候再让它继续执行,如果超时或者是遇到断言的失败,该用例会失败。

注意:使用pod的项目中,在XC测试框架中测试内容包括第三方包时,需要手动去设置Header Search Paths才能找到头文件

  1. expectationForNotification 方法 ,该方法监听一个通知,如果在规定时间内正确收到通知则测试通过。
#define WAIT do {\
[self expectationForNotification:@"MJUnitTest" object:nil handler:nil];\
[self waitForExpectationsWithTimeout:30 handler:nil];\
} while (0);
// waitForExpectationsWithTimeout是等待时间,超过了就不再等待往下执行。
#define NOTIFY \
[[NSNotificationCenter defaultCenter]postNotificationName:@"MJUnitTest" object:nil];

- (void)testRequest{
    
    AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
    manager.responseSerializer.acceptableContentTypes = [NSSet setWithObject:@"text/html"];
    NSString *urlStr = @"http://www.weather.com.cn/data/cityinfo/101190401.html";
    [manager GET:urlStr parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        
        NSLog(@"responseObject:%@",responseObject);
        XCTAssertNotNil(responseObject, @"返回出错");
        NOTIFY //继续执行
        
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        
        NSLog(@"error:%@",error);
        XCTAssertNil(error, @"请求出错");
        NOTIFY //继续执行
        
    }];
    WAIT   //暂停
}

2.expectationWithDescription 来进行异步是否完成期望的测试。

- (void)testRequest2{
    
    XCTestExpectation *exp = [self expectationWithDescription:@"接口请求失败。。。"];
    
    AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
    manager.responseSerializer.acceptableContentTypes = [NSSet setWithObject:@"text/html"];
    NSString *urlStr = @"http://www.weather.com.cn/data/cityinfo/101190401.html";
    [manager GET:urlStr parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        
        NSLog(@"responseObject2:%@",responseObject);
        XCTAssertNotNil(responseObject, @"返回出错");
        //如果断言没问题,就调用fulfill宣布测试满足
        [exp fulfill];
        
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        
        NSLog(@"error:%@",error);
        XCTAssertNil(error, @"请求出错");
        
        [exp fulfill];
        
    }];
    
    //设置延迟多少秒后,如果没有满足测试条件就报错
    [self waitForExpectationsWithTimeout:15 handler:^(NSError * _Nullable error) {
        if (error) {
            NSLog(@"Timeout Error: %@", error);
        }
    }];
}

3.expectationForPredicate测试方法,代码来自于AFNetworking,用于测试backgroundImageForState方法

- (void)testThatBackgroundImageChanges {
    XCTAssertNil([self.button backgroundImageForState:UIControlStateNormal]);
    NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(UIButton  * _Nonnull button, NSDictionary<NSString *,id> * _Nullable bindings) {
            return [button backgroundImageForState:UIControlStateNormal] != nil;
    }];

    [self expectationForPredicate:predicate
              evaluatedWithObject:self.button
                          handler:nil];
    [self waitForExpectationsWithTimeout:20 handler:nil];
}

利用谓词计算,button是否正确的获得了backgroundImage,如果正确20秒内正确获得则通过测试,否则失败。

3.性能测试

将要测量执行时间的代码放到testPerformanceExample方法内部的block中:

- (void)testPerformanceExample {
    
    [self measureBlock:^{
        
        NSMutableArray * mutArray = [[NSMutableArray alloc] init];
        for (int i = 0; i < 10000; i++) {
            NSObject * object = [[NSObject alloc] init];
            [mutArray addObject:object];
        }
    }];
}

在block中写一个for循环执行10000次,然后点击方法左边的菱形图标,得到:average: 0.003sec

图11.png

也可以从控制台打印信息获取程序运行10次的时间,取一个平均运行时间值:

measured [Time, seconds] average: 0.003, relative standard deviation: 9.329%,   
values: [0.002840, 0.002487, 0.003074, 0.002515, 0.002386, 0.002313, 0.002351, 0.002362, 0.002455, 0.002741], 

五、断言

XCTFail(format…) 生成一个失败的测试; 
XCTAssertNil(a1, format...)为空判断,a1为空时通过,反之不通过;
XCTAssertNotNil(a1, format…)不为空判断,a1不为空时通过,反之不通过;
XCTAssert(expression, format...)当expression求值为TRUE时通过;
XCTAssertTrue(expression, format...)当expression求值为TRUE时通过;
XCTAssertFalse(expression, format...)当expression求值为False时通过;
XCTAssertEqualObjects(a1, a2, format...)判断相等,[a1 isEqual:a2]值为TRUE时通过,其中一个不为空时,不通过;
XCTAssertNotEqualObjects(a1, a2, format...)判断不等,[a1 isEqual:a2]值为False时通过;
XCTAssertEqual(a1, a2, format...)判断相等(当a1和a2是 C语言标量、结构体或联合体时使用, 判断的是变量的地址,如果地址相同则返回TRUE,否则返回NO);
XCTAssertNotEqual(a1, a2, format...)判断不等(当a1和a2是 C语言标量、结构体或联合体时使用);
XCTAssertEqualWithAccuracy(a1, a2, accuracy, format...)判断相等,(double或float类型)提供一个误差范围,当在误差范围(+/-accuracy)以内相等时通过测试;
XCTAssertNotEqualWithAccuracy(a1, a2, accuracy, format...) 判断不等,(double或float类型)提供一个误差范围,当在误差范围以内不等时通过测试;
XCTAssertThrows(expression, format...)异常测试,当expression发生异常时通过;反之不通过;(很变态) XCTAssertThrowsSpecific(expression, specificException, format...) 异常测试,当expression发生specificException异常时通过;反之发生其他异常或不发生异常均不通过;
XCTAssertThrowsSpecificNamed(expression, specificException, exception_name, format...)异常测试,当expression发生具体异常、具体异常名称的异常时通过测试,反之不通过;
XCTAssertNoThrow(expression, format…)异常测试,当expression没有发生异常时通过测试;
XCTAssertNoThrowSpecific(expression, specificException, format...)异常测试,当expression没有发生具体异常、具体异常名称的异常时通过测试,反之不通过;
XCTAssertNoThrowSpecificNamed(expression, specificException, exception_name, format...)异常测试,当expression没有发生具体异常、具体异常名称的异常时通过测试,反之不通过

文章附件:Demo

附录:本来写好了,去开了个需求会议,回来打开页面,内容丢了一半,(╥╯^╰╥),历史版本也没保存,不得不重新写了一遍,下次一定要备份,看在辛苦的份上,喜欢的点个赞吧~

参考文章:
iOS单元测试(作用及入门提升)
浅谈iOS单元测试
iOS单元测试初探以及OCMock使用入门
iOS-使用Xcode自带单元测试UnitTest
iOS - UnitTests 单元测试
iOS单元测试

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

推荐阅读更多精彩内容