iOS开发人员需要具备的App单元测试技能

使用Xcode获取单元测试覆盖情况

Xcode中cmd+U运行单元测试,运行结束以后通过cmd+8可以查看测试用例的通过情况。如果要查看每个源文件的代码覆盖情况,需要进行如下配置:
Product-->Scheme-->Edit Scheme,选择左侧的Test,查看info选项,设置成如下形式:

14910318019460.jpg

再次执行单元测试,在下图中即可查看到源文件的覆盖情况,点击某个文件上的小箭头可以查看该文件每一行的覆盖情况。


14910321473083.jpg

使用XcodeCoverage获得统计数据

遗憾的是Xcode并没有提供和整个项目相关的覆盖统计数据。但是没有关系,使用XcodeCoverage就可以做到,XcodeCoverage是一系列脚本的集合,生成的报告如下图所示:

14910324796616.jpg

如果这正是你需要的,那么不要着急,经过如下几步简单的设置,就可以得到这个统计结果了:

1.工程对应的target-->Build setting,设置Generate Legacy Test Coverage File 和 Instrument Program Flow两个参数。

14910329089909.jpg
14910329296074.jpg

2.到这里下载XcodeCoverage代码并拷贝文件夹XcodeCoverage至工程目录下(不需要添加到工程中),当然现在也支持使用pod直接设置。同时增加一个.xcodecoverageignore文本文件到至工程目录下。

14910332809587.jpg

其中.xcodecoverageignore用于存放统计覆盖率时需要排除的文件(如第三方库、类似于.lproj的工程文件等),需要注意的是如果需要排除一整个文件夹,他所包含的所有子文件夹也要逐层列出来。${SRCROOT}指.xcodecoverageignore的父文件夹。

${SRCROOT}/Your_Proj_name/*
${SRCROOT}/Your_Proj_name/Base/*
${SRCROOT}/Your_Proj_name/Base.lproj/*
${SRCROOT}/Your_Proj_name/Image/*
${SRCROOT}/Your_Proj_name/Images.xcassets/*
${SRCROOT}/Your_Proj_name/Module/*
${SRCROOT}/Your_Proj_name/Third/*
${SRCROOT}/Your_Proj_name/zh-Hans.lproj/*
${SRCROOT}/Your_Proj_name/Base/CycleScrollView/CollectionViewCell.h

3.工程对应的target-->buildphase 新增new run script phase, 内容如下:(工程对应的那个target)

${SRCROOT}/XcodeCoverage/exportenv.sh

设置后的结果如下:

14910337891793.jpg

4.运行单元测试,在Terminal中进入XcodeCoverage目录,执行下面的命令XcodeCoverage就会启动,运行结束后自动弹出统计结果。

./getcov -s

5.如果需要清空当前的统计结果,只要执行如下命令即可:

./cleancov

常用的测试方法

1.使用Exceptation的场景,如网络请求[4],常用的是 XCTestExpectation, expectationForPredicate和expectationForNotification三种。

  • 使用XCTestExpectation实现网络请求等待
 - (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);
        }
    }];
}

这个测试肯定是通过的,因为设置延迟为3秒,而异步操作2秒就除了一个正确的结果,并宣布了条件满足 [exp fulfill],但是当我们把延迟改成1秒,这个测试用例就不会成功,错误原因是 expectationWithDescription:@"这里可以是操作出错的原因描述。。。

  • 下面这个例子使用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秒内正确获得则通过测试,否则失败。

  • expectationForNotification 方法 ,该方法监听一个通知,如果在规定时间内正确收到通知则测试通过。
- (void)testAsynExample1 {
    [self expectationForNotification:(@"监听通知的名称xxx") object:nil handler:nil];
    [[NSNotificationCenter defaultCenter]postNotificationName:@"监听通知的名称xxx" object:nil];

    //设置延迟多少秒后,如果没有满足测试条件就报错
    [self waitForExpectationsWithTimeout:3 handler:nil];
}

这个例子也可以用expectationWithDescription实现,只是多些很多代码而已,但是这个可以帮助你更好的理解 expectationForNotification 方法和 expectationWithDescription 的区别。同理,expectationForPredicate方法也可以使用expectationWithDescription实现。

 func testAsynExample1() {
    let expectation = expectationWithDescription("监听通知的名称xxx")
    let sub = NSNotificationCenter.defaultCenter().addObserverForName("监听通知的名称xxx", object: nil, queue: nil) { (not) -> Void in
        expectation.fulfill()
    }
    NSNotificationCenter.defaultCenter().postNotificationName("监听通知的名称xxx", object: nil)
    waitForExpectationsWithTimeout(1, handler: nil)
    NSNotificationCenter.defaultCenter().removeObserver(sub)
}

2.需要测试私有属性或者方法
--在测试类中添加所测试类的category

@interface TTViewController(UnitTest)
@property (nonatomic, strong) NSString *privatePropertyInTTVC;
- (void) privateMethodInTTViewController;
@end

3.性能测试[4]
性能测试主要使用 measureBlock 方法,用于测试一组方法的执行时间,通过点击meaure block左侧边栏符号可以设置baseline(基准)和stddev(标准偏差),来判断方法是否能通过性能测试。

- (void)testPerformanceExample {
    // This is an example of a performance test case.
    [self measureBlock:^{
          //Put the code you want to measure the time of here.
          //你的性能测试的代码放在这里
    }];
}

使用OCMock可以参考这里

常用的断言

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. 使用的第三方SDK要求真机才能够通过编译。
    使用如下宏定义,保证使用该SDK的代码只在非模拟器的情况下才被编译。如果设置正确,在Xcode中选择模拟器运行时该部分代码的高亮显示会消失。
 #if !(TARGET_IPHONE_SIMULATOR)
 //your code here
 #endif

也可以直接采用真机运行单元测试,只是通过XcodeCoverage获取结果稍微麻烦一点,具体可以参考这里。(动手测试了一下发现得到的结果和Xcode中看到的覆盖率数据不一致,不明所以)。

  1. 主工程中带有Pod引入的库文件需要进行配置,具体可以参考这里

ref:

  1. https://testerhome.com/topics/6644 iOS (Object-C) 非单元测试状态下代码覆盖率获取尝鲜
  2. https://my.oschina.net/ChenTF/blog/677309 [基础]iOS 单元测试(一)入门与配置
  3. http://www.jianshu.com/p/8bbec078cabe iOS单元测试(作用及入门提升)
  4. http://liuyanwei.jumppo.com/2016/03/10/iOS-unit-test.html iOS单元测试
  5. http://luoxianming.cn/2016/05/25/ocmock/ OCMock进行mock,以实现实现更加方便快捷的单元测试
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,525评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,203评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,862评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,728评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,743评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,590评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,330评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,244评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,693评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,885评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,001评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,723评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,343评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,919评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,042评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,191评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,955评论 2 355

推荐阅读更多精彩内容