iOS UnitTest单元测试

一、单元测试的定义

在计算机编程中,单元测试(英语:Unit Testing)又称为模块测试, 是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。

在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。

根据不同场景,单元的定义也不一样,通常我们将C语言的单个函数或者面向对象语言的单个类视作测试的单元。在使用单元测试的过程中,我们要知道这一点:

单元测试并不是为了证明代码的正确性,它只是一种用来帮助我们发现错误的手段

单元测试不是万能药,它确实能帮助我们找到大部分代码逻辑上的bug,同时,为了提高测试覆盖率,这能逼迫我们对代码不断进行重构,提高代码质量等。

二、iOS单元测试

xcode本身的测试框架集成:在Xcode4.x中集成了测试框架OCUnit,UI Tests是iOS9推出的新特性。目前我们在创建项目的时候会默认选中有关测试的这两项:Include Unit Tests、Include UI Tests。在创建项目之后,会自动生成一个appName+Tests的文件夹目录,下面存放着单元测试的文件。

根据测试的目的大致可以将单元测试分为这三类:
a.性能测试:测试代码执行花费的时间
b.逻辑测试:测试代码执行结果是否符合预期
c.异步测试:测试多线程操作代码

UnitTest文件里面方法介绍:

- (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 {//用来执行我们需要的测试操作,正常情况下,我们不使用这个方法,而是创建名为test+测试目的的方法来完成我们需要的操作(注意:此时自定义的方法需要以test开头方能进行测试,否则左边是不显示菱形的)
    // This is an example of a functional test case.
    // Use XCTAssert and related functions to verify your tests produce the correct results.
}

\- (void)testPerformanceExample {//会将方法中的block代码耗费时长打印出来--默认执行了10次,打印出了平均耗时,和各次的耗时,最大误差不超过10%。其中运行之后block这行右侧显示的就是平均耗时。
    // This is an example of a performance test case.
    [self measureBlock:^{
        // Put the code you want to measure the time of here.
    }];
}

在每个测试用例方法的左侧有个菱形的标记,点击这个标记可以单独的运行这个测试方法。如果测试通过没有发生任何断言错误,那么这个菱形就会变成绿色勾选状态。使用快捷键command+U直接依次调用所有的单元测试。

另外,可以在左侧的文件栏中选中单元测试栏目,然后直观的看到所有测试的结果。同样的点击右侧菱形位置的按钮可以运行单个测试方法或者文件:


01.png

为了保证单元测试的正确性,我们应当保证测试用例中只存在一个类或者只发生一个类变量的属性修改。下面是我们测试中常用的宏定义:(XCTest 带有许多内建的断言)

XCTAssertNotNil(a1, format…) 当a1不为nil时成立
XCTAssert(expression, format...) 当expression结果为YES成立
XCTAssertTrue(expression, format...) 当expression结果为YES成立;
XCTAssertEqualObjects(a1, a2, format...) 判断相等,当[a1 isEqualTo: a2]返回YES的时候成立
XCTAssertEqual(a1, a2, format...) 当a1==a2返回YES时成立
XCTAssertNotEqual(a1, a2, format...) 当a1!=a2返回YES时成立
</br>
&&

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没有发生具体异常、具体异常名称的异常时通过测试,反之不通过

三、测试
1、逻辑测试:逻辑测试的目的是为了检测在代码执行前后发生的变化是否符合预期

e.g:

@interface TestModel1 : NSObject

@property (nonatomic, copy) NSString * name;
@property (nonatomic, strong) NSNumber * age;
@property (nonatomic, assign) NSUInteger flags;
+ (instancetype)modelWithName: (NSString *)name age: (NSNumber *)age flags: (NSUInteger)flags;
- (instancetype)initWithDictionary: (NSDictionary *)dict;
- (NSDictionary *)modelToDictionary;

@end

@implementation TestModel1

+ (instancetype)modelWithName:(NSString *)name age:(NSNumber *)age flags:(NSUInteger)flags
{
    TestModel1 *model = [[self alloc] init];
    model.name = name;
    model.age = age;
    model.flags = flags;
    return model;
    
}

- (instancetype)initWithDictionary: (NSDictionary *)dict
{

    self.name = dict[@"name"];
    self.age  = dict[@"age"];
    self.flags = [dict[@"flags"] integerValue];
    
    return self;
}

- (NSDictionary *)modelToDictionary
{
    return @{@"name":self.name,@"age":self.age,@"flags":[NSNumber numberWithInteger:self.flags]};
}

@end

然后在测试文件里面:

\- (void)testModelConvert
{
    NSString * json = @"{\"name\":\"SindriLin\",\"age\":22,\"flags\":987654321}";
    NSMutableDictionary * dict = [[NSJSONSerialization JSONObjectWithData: [json dataUsingEncoding: NSUTF8StringEncoding] options: kNilOptions error: nil] mutableCopy];
    TestModel1 * model = [[TestModel1 alloc] initWithDictionary: dict];
    XCTAssertNotNil(model);
    XCTAssertTrue([model.name isEqualToString: @"SindriLin"]);
    XCTAssertTrue([model.age isEqual: @(22)]);
    XCTAssertEqual(model.flags, 987654321);
    XCTAssertTrue([model isKindOfClass: [TestModel1 class]]);
    model = [TestModel1 modelWithName: @"Tessie" age: dict[@"age"] flags: 562525];
    XCTAssertNotNil(model);
    XCTAssertTrue([model.name isEqualToString: @"Tessie"]);
    XCTAssertTrue([model.age isEqual: dict[@"age"]]);
    XCTAssertEqual(model.flags, 562525);
    NSDictionary * modelJSON = [model modelToDictionary];
    XCTAssertTrue([modelJSON isEqual: dict] == NO);
    dict[@"name"] = @"Tessie";
    dict[@"flags"] = @(562525);
    XCTAssertTrue([modelJSON isEqual: dict]);
}
2、性能测试:

在平常的工作中,我们还可以通过: instrument(xcode->product->profile)工具很好的查找到项目中的代码耗时点,(后面介绍)。先介绍单元测试的性能测试:

测试文件:

\- (void)testPerformanceExample {//会将方法中的block代码耗费时长打印出来--默认执行了10次,打印出了平均耗时,和各次的耗时,最大误差不超过10%。
    // This is an example of a performance test case.
    [self measureBlock:^{
        // Put the code you want to measure the time of here.
        [TestModel1 randomModels];
//        for (int i = 0; i<100; ++i) {
//            NSLog(@"wgj:%d",i);
//        }
    }];
}

自定义的model文件中添加:

\+ (NSArray<TestModel1 *> *)randomModels
{
    NSMutableArray * models = @[].mutableCopy;
    NSArray * names = @[
                        @"xiaoli01", @"xiaoli02", @"xiaoli03", @"xiaoli04", @"xiaoli05"
                        ];
    NSArray * ages = @[
                       @15, @20, @25, @30, @35
                       ];
    NSArray * flags = @[
                        @123, @456, @789, @012, @234
                        ];
    for (NSUInteger idx = 0; idx < 100; idx++) {
        TestModel1 * model = [self modelWithName: names[arc4random() % names.count] age: ages[arc4random() % ages.count] flags: [flags[arc4random() % flags.count] unsignedIntegerValue]];
        [models addObject: model];
        [NSThread sleepForTimeInterval: 0.01];
    }
    return models;
}
02.png

在平常的test方法中,也会打印测试方法的执行时间,例如下面,但是没有上面这种在性能测试方法中测的准确。


01.png

打印台会打印各测试方法的耗时,直接使用单元测试来获取某段代码的执行时间要比使用instrument快的多(instrument定位更精确)。通过性能测试直观的获取执行时间后,我们可以根据需要来决定是否将这些代码放到子线程中执行来优化代码(很多时候,数据转换会占用大量的CPU计算资源)

3.异步测试

在Xcode 6之前的版本里面并没有内置XCTest,想使用Xcode测试的只能是在主线程的RunLoop里面使用一个while循环,然后一直等待响应或者直到timeout.(Xcode 6中添加了新特性:XCTestExpectation 和性能测试:特性是内建的对于异步测试的支持,测试能够为了确定的合适的条件等待一个指定时间长度,而不需要求助于GCD)

e.g.老方法:
\- (void)testAsync
{// 异步测试
    NSDictionary * dict = @{
                            @"name": @"MrLi",
                            @"age": @28,
                            @"flags": @987
                            };
    TestModel1 * model = [[TestModel1 alloc] initWithDictionary: dict];
    XCTAssertNotNil(model);
    [model asyncConvertToData];
    while (model.data == nil) {
        CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.01, YES);
        NSLog(@"waiting");
    }
    XCTAssertNotNil(model.data);
    NSLog(@"convert finish %@", model.data);
}

model文件:

\- (void)asyncConvertToData
{
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSDictionary * modelJSON = nil;
        for (NSInteger idx = 0; idx < 20; idx++) {
            modelJSON = [self modelToDictionary];
            [self setValuesForKeysWithDictionary: modelJSON];
            [NSThread sleepForTimeInterval: 0.001];
        }
        _data = [NSJSONSerialization dataWithJSONObject: modelJSON options: NSJSONWritingPrettyPrinted error: nil];
    });
}
e.g.新方法

在Xcode 6里,苹果以XCTestExpection类的方式向XCTest框架里添加了测试期望(test expection)。当我们实例化一个测试期望(XCTestExpectation)的时候,测试框架就会预计它在之后的某一时刻被实现。最终的程序完成代码块中的测试代码会调用XCTestExpection类中的fulfill方法来实现期望。
我们让测试框架等待(有时限)测试期望通过XCTestCase的waitForExpectationsWithTimeout:handler:方法实现。如果完成处理的代码在指定时限里执行并调用了fulfill方法,那么就说明所有的测试期望在此期间都已经被实现。此方法中的handler的参数其实是一个block,block中若是写有代码,代码执行的条件(满足其中之一就可执行):a、所有期望在指定的时间内都以实现; b、期望在指定的时间内没有实现(此时会报错,但是block里面的方法会执行)。
代码:
<pre>
- (void)testAsyncOutTime{

XCTestExpectation *ex = [self expectationWithDescription:@"wgj001"];

NSURL *url = [NSURL URLWithString:@"https://www.baidu.com"];

NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];

NSURLRequest *request = [NSURLRequest requestWithURL:url];

NSURLSessionTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
    NSLog(@"url请求完成");
    [ex fulfill];//如果完成处理的代码在指定时限里执行并调用了fulfill方法,那么就说明所有的测试期望在此期间都已经被实现
    
}];
[task resume];

[self waitForExpectationsWithTimeout:1 handler:^(NSError * _Nullable error) {
    if (error) {
        NSLog(@"wati:%@",error);
    }

// [task cancel];
NSLog(@"url请求超时-结束");
}];

}

</pre>

断言(详解):

<pre>
1、XCTFail(...):参数可有可无,若有则须是字符串,参数为错误的描述。无条件的都是测试失败。在测试驱动里有这么个情况,你定义了测试方法,但是没有给出具体的实现。那么你不会希望这个测试能通过的。一般被用作一个占位断言。等你的测试方法完善好了之后再换成最贴近你的测试的断言。有或者,在某些情况下else了之后就是不应该出现的情况。那么这个时候可以把XCTFail放在这个else里面。
2、XCTAssertNil(expression, ...)/XCTAssertNotNil(expression, ...):判断给定的表达式值是否为nil, XCTAssertNil(表达式为nil的时候通过),XCTAssertNotNil(表达式不为nil的时候通过),其中...是错误描述,为字符串类型,下面的表达式中的意思都是一样的。
3、XCTAssert(expression, ...):如果expression(表达式)执行的结果为true的话,这个测试通过。否则,测试失败,并在console中输出后面的format字符串.
4、后面基于XCTAssert演化出来的断言,不仅可以满足测试的需求而且可以更好更明确的表达出你要测试的是什么。最好是使用这些演化出来的断言:

a. Bool测试
对于bool型的数据,或者只是简单的bool型的表达式,使用XCTestAssertTrue或者XCTestAssertFalse:
XCTAssertTrue(expression, format...)
XCTAssertFalse(expression, format...)

b. 相等测试
测试两个数值的值是否相等使用XCTAssert[Not]Equal:
XCTAssertEqual(expression1, expression2, format...)
XCTAssertNotEqual(expression1, expression2, format...);
判断两个对象用:XCTAssertEqualObjects(expression1, expression2, ...)和XCTAssertNotEqualObjects(expression1, expression2, ...)

在Double、Float型数据的对比中使用XCTAssert[Not]EqualWithAccuracy来处理浮点精度的问题:
XCTAssertEqualWithAccuracy(expression1, expression2, accuracy, format...)
XCTAssertNotEqualWithAccuracy(expression1, expression2, accuracy, format...)
e.g. XCTAssertEqualWithAccuracy(12, 14, 1,@"wgj"),则不通过,因为12和14的差别已经超过了设定的值1。

XCTAssertGreaterThan[OrEqual] & XCTAssertLessThan[OrEqual], 和下面的条件操作符比较的是一个意思 == with >, >=, <, 以及 <=

5、抛异常:
a.
XCTAssertThrows(expression, ...):表达式抛异常时,通过;反之,不通过。e.g. XCTAssertThrows([model onlyTest],@"wgj01");方法在model中只有声明但没有实现,此表达式是会异常的,但是这句测试的代码则是通过的。
b.
XCTAssertThrowsSpecific(expression, exception_class, ...):表达式 抛出异常,并且抛出的异常类属于NSException,才会执行通过;反之。
e.g.
XCTAssertThrowsSpecific([model onlyTest02],NSException,@"wgj001");--onlyTest02方法实现时:运行崩溃;onlyTest02方法未实现时,执行未实现的方法,系统会自动生成NSException类型的异常,符合定义的NSException类,测试代码运行通过。
c.
XCTAssertThrowsSpecificNamed(expression, exception_class, exception_name, ...):表达式抛出异常,并且抛出的异常类属于NSException,并且异常类的名字符合定义的名字时,才会执行通过;反之。
e.g.
XCTAssertThrowsSpecificNamed([model onlyExceptionTest], NSException, @"自定义异常",@"wgj002");
model中的实现方法:

  • (void)onlyExceptionTest{
    NSException *exx = [NSException exceptionWithName:@"自定义异常" reason:@"崩溃test02" userInfo:@{@"key02":@"value02"}];

      @throw exx;
    

}
此种,测试代码是通过的。
若改为:
XCTAssertThrowsSpecificNamed([model onlyExceptionTest], NSException, @"随便写的名字",@"wgj002");则测试代码不通过,因为异常名字不匹配。
</pre>

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

推荐阅读更多精彩内容