iOS 单元测试 - XCTest


简介

单元测试(Unit Testing)又称为模块测试,是针对程序模块软件设计来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。对于面向对象编程,最小单元就是方法,包括基类、抽象类、或者派生类中的方法。

单元测试通常由软件开发人员编写,用于确保他们所写的代码符合软件需求和遵循开发目标。通常来说,每修改一次程序就会进行最少一次单元测试,在编写程序的过程中前后很可能要进行多次单元测试,以证实程序达到工作目标要求。

Xcode 集成了对单元测试的支持 XCTest。XCTest 是从 Xcode5 开始引入的一个测试框架,是上一代测试框架 OCUnit 的更现代化实现。XCTest 提供了与 Xcode 更好的集成。下面我们简单介绍下XCTest的使用。

XCTest

在 Xcode 新建项目时,勾选 Unit Tests 和 UI Tests,会创建对应的测试 target,并创建了继承于XCTestCase 的测试用例类,该类继承自 XCTestCase 类,其中包含三个方法:setUp,tearDown和 testExample。

  • setUp 用于在测试前设置好需要用到的对象等
  • tearDown 在测试结束时调用
  • testExample 是一个测试方法,测试方法命名通常是 testXXX 的格式,且不能有参数,不然不会识别为测试方法,测试方法的执行顺序是按照方法名中 test 后面的字符顺序执行的。
  • measureBlock: 性能测试方法,将需要性能测试的代码放入 block 里,运行这个方法会执行多次,运行时间比对设定的标准值和偏差判断是否可以通过测试

创建完成后,就可以在测试方法里,编写测试代码,然后点击方法前的菱形按钮运行测试方法, 也可以使用快捷键 command+u 运行整个测试单元。正确运行后显示绿色对勾,运行错误会显示红色叉号。


断言

大部分的测试方法使用断言决定的测试结果。所有断言都有一个类似的形式:比较,表达式为真假,强行失败等。

XCTFail(format...)  直接Fail
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是标量、结构体或联合体时使用,实际测试发现NSString也可以);
XCTAssertNotEqual(a1, a2, format...)判断不等(当a1和a2是标量、结构体或联合体时使用);
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没有发生异常时通过测试;
XCTAssertNoThrowSpecificNamed(expression, specificException, exception_name, format...)异常测试,当expression没有发生具体异常、具体异常名称的异常时通过测试,反之不通过

自定义断言宏
在使用断言时,经常使用一些特定情况的断言,写非常的啰嗦,难以阅读。并且还都是重复代码。可以通过编写自己的断言宏来解决这个问题。例如:

NSString *string = @"http";
XCTAssertTrue([string isKindOfClass:[NSString class]] && [string hasPrefix:@"http"],
    @"'%@' is not a valid URL string", string);


//自定义断言
#define AssertIsValidURLString(a) \
if (![a isKindOfClass:[NSString class]] || ![a hasPrefix:@"http"]) { \
    XCTFail(@"'%@' is not a valid URL string", a); \
}\

NSString *text = @"123";
AssertIsValidURLString(text);

对于更复杂的断言和检查,可以使用简单的辅助类,方便检查。

异步测试

测试异步方法时,例如网络请求等耗时操作,由于执行结果不是立即就能获取到,XCTest 提供了一些辅助方法,如下例所示:

- (void)testAsynExample {
    XCTestExpectation *expectation = [self expectationWithDescription:@"操作超时。。"];
    NSOperationQueue *queue = [[NSOperationQueue alloc]init];
    [queue addOperationWithBlock:^{
        sleep(2); //模拟耗时操作
        [expectation fulfill];
        XCTAssert(YES, @"fail"); //判断异步方法的结果是否正确
    }];

    //等待 XCTestExpectation fulfill,设置延时等待多少秒,如果超时就报错
    [self waitForExpectationsWithTimeout:1 handler:^(NSError * _Nullable error) {
        if (error) {
            NSLog(@"Error: %@", error);
        }
    }];
}

waitForExpectationsWithTimeout: 方法会在规定时间内,等待期望 XCTestExpectation 满足 fulfill,规定时间内不满足期望就会报错。

异步测试除了使用 expectationWithDescription 以外,还可以使用 expectationForPredicate 和 expectationForNotification

  • expectationForPredicate
- (void)testAsynExample {
    XCTAssertNil(self.imageView.image);
    [self.imageView setImageWithURL:self.jpegURL];
    
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"image != nil"];
    [self expectationForPredicate:predicate evaluatedWithObject:self.imageView handler:nil];
    [self waitForExpectationsWithTimeout:10 handler:nil];
}

NSPredicate 谓词判断,是否加载出了图片,self.imageView.image != nil,在规定时间内是否测试通过。

  • expectationForNotification 监听一个通知,在规定时间内等待,是否收到通知
- (void)testAsynExample {
    //....
    [self expectationForNotification:@"NotificationName" object:nil handler:nil];
    [self waitForExpectationsWithTimeout:10 handler:nil];
}

UITest

上面介绍的单元测试是对 app 的业务逻辑以及网络接口方面的测试。下面来介绍一下 UI 的测试。 在创建项目时勾选 UI Tests 会创建对应的 UI 测试的 target,如果你要在已有项目中添加 UI Tests 的话,可以新建一个 iOS UI Testing 的 target。创建完成后和上面一样也会创建对应的继承于 XCTestCase 测试类。

UI 行为录制

写好 UI 后就可以,进行我们的 UI 测试了,在 setUp 中,我们使用 XCUIApplication 的 launch 方法来启动测试 app。XCUIApplication 是 UIApplication 在测试进程中的代理 (proxy),我们可以在 UI 测试中通过这个类型和应用本身进行一些交互,比如开始或者终止一个 app。
然后使用 Xcode 的 UI Testing 直接录制操作,操作如下:

点击录制按钮,启动 app,点击 UI 就会在测试方法中,生成对应的测试代码,看起来很厉害的样子。

获取 UI 元素

在录制时,点击输入框,可以看到获取 UI 元素的代码,如下:

- (void)testExample {
    XCUIApplication *app = [[XCUIApplication alloc] init];
    XCUIElement *element = [[[[[[[[[app childrenMatchingType:XCUIElementTypeWindow] elementBoundByIndex:0] childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element;
    [[element childrenMatchingType:XCUIElementTypeTextField].element tap];
    [[element childrenMatchingType:XCUIElementTypeSecureTextField].element tap];
    
    [app.buttons[@"login"].staticTexts[@"login"] tap];
}

自动录制生成的代码使用了很多 query 来查询文本框,获取代表 app 中具体 UI 元素的 XCUIElement,然后对其进行测试操作。但是这样产生大量代码,难以理解,我们可使用简洁的方法获取 UI 元素。
在 Interface Builder 或者代码中进行设置 textfield 的 identifier :

- (void)testExample {
    
    NSString *name = @"admin";
    NSString *pwd = @"123";
    
    XCUIApplication *app = [[XCUIApplication alloc] init];
    //获取 name 输入框
    XCUIElement *nameTextField = app.textFields[@"nameTextField"];
    [nameTextField tap];
    [nameTextField typeText:name]; //输入框中写入文字
    
    //获取 pwd 输入框
    XCUIElement *pwdTextField = app.secureTextFields[@"pwdTextField"];
    [pwdTextField tap];
    [pwdTextField typeText:pwd];
    
    //点击 login 按钮
    [app.buttons.staticTexts[@"login"] tap];
    
    //登录需要网络请求,等待一段时间。登录成功 push 到下一个页面
    //这里判断在规定的时间内导航栏是否 push 过去
    XCUIElement *nav = app.navigationBars[name].staticTexts[name];
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"exists == 1"];
    [self expectationForPredicate:predicate evaluatedWithObject:nav handler:nil];
    [self waitForExpectationsWithTimeout:6 handler:nil];
}

上面的操作是获取两个输入框,并写入内容,点击登录 push 到下一个页面。

总结

本篇文章介绍了,使用 Xcode 来进行单元测试的一些操作,可以看到还是很方便快捷的。熟练掌握单元测试的一些技巧,对于提高 app 的质量还是有很大帮助的。

References

iOS单元测试
XCTest 测试实战
WWDC15 Session笔记 - Xcode 7 UI 测试初窥

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

推荐阅读更多精彩内容

  • 前言 单元测试简单来说,就是为了方便测试一些功能是否正常运行,调试接口是否能正常使用,用代码去检测代码是否正确的一...
    minjing_lin阅读 4,107评论 0 20
  • 单元测试在维基百科上的解释 在计算机编程中,单元测试(英语:Unit Testing)又称为模块测试, 是针对程序...
    愚虫阅读 808评论 0 0
  • 前言:测试是一个好的App不可缺少的部分。每一个App都是由一个个小的功能组合到一起的。而这些小的功能又是由一个个...
    伯牙呀阅读 6,693评论 1 19
  • 简介 测试目的:模拟多种可能性,减少错误,增强健壮性,提高稳定性。 测试种类:在iOS中的通常分为单元测试和UI测...
    i顺颂时宜阅读 9,109评论 0 39
  • 一、简介 单元测试(Unit Testing) 是一种软件测试方法,主要用于确定各个独立的软件模块是否正确。在这个...
    阳光下的灰尘阅读 1,186评论 0 2