单元测试

  • 测试导航(test navigator)

当进行测试时,你会经常使用到Xcode的测试导航(test navigator)。
测试导航(test navigator)是workspace的一部分,使创建、管理、运行和评估测试更加加单。通过点击导航选择栏中的图标进入测试,该图标在问题导航和调试导航之间。
![查看测试用例](http://ogpt2m9nl.bkt.clouddn.com/屏幕快照 2017-03-26 下午4.45.25.png)
注意:Xcode 测试目标(target)生成的测试包(test bundle)显示在测试导航(test navigator)处。

![测试目标2](http://ogpt2m9nl.bkt.clouddn.com/屏幕快照 2017-03-26 下午4.46.15.png)

绿色为测试通过、红色为不通过

![运行测试2](http://ogpt2m9nl.bkt.clouddn.com/屏幕快照 2017-03-26 下午4.46.41.png)

灰色菱形代表性能测试 这也就能说明这里为什么没有通过或是失败的断言点击此处会出现性能测试结果面板,此面板允许设置性能基准以及编辑基线和最大参数
![测试结果面板](http://ogpt2m9nl.bkt.clouddn.com/屏幕快照 2017-03-26 下午4.46.50.png)

关于测试文件方法

  • 所有的测试用例方法保证以test开头
  • setUp在测试用例之前调用的初始化方法tearDown释放测试用例资源在每个测试用例完成后调用testExample例子testPerformanceExample测试性能的例子,在block内写测试性能的代码,设置baseline(基准)和stddev(标准偏差)来判断方法是否能通过性能测试。
- (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.
}

- (void)testPerformanceExample {
    // This is an example of a performance test case.
    [self measureBlock:^{
        // Put the code you want to measure the time of here.
    }];
}

断言

XCTest

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语言标量、结构体或联合体时使用,实际测试发现NSString也可以);

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

特别注意下XCTAssertEqualObjects和XCTAssertEqual。
XCTAssertEqualObjects(a1, a2, format...)的判断条件是[a1 isEqual:a2]是否返回一个YES。XCTAssertEqual(a1, a2, format...)的判断条件是a1 == a2是否返回一个YES。对于后者,如果a1和a2都是基本数据类型变量,那么只有a1 == a2才会返回YES。

示例如下:

  • 断言:
- (void)testExample {
    //设置变量和预期效果
    NSUInteger a = 10;NSUInteger b = 15;
    NSUInteger c = 24;
    //执行方法得到
    NSUInteger sum = [self sum:a b:b];
    //断言判定实际值和预期是否符合
    XCTAssertEqual(sum, c,@"测试不通过");
}
-(NSUInteger)sum:(NSUInteger)a b:(NSUInteger)b{
    return a+b;
}
  • 性能

关于性能测试:设置baseline(基准)和stddev(标准偏差)来判断方法是否能通过性能测试。
就比如说关于网络请求通常我们给出一个标准,多长时间内请求到结果,能够接受的偏差是多少。那么就可以设置一个基准盒偏差来验证这个请求。

  • 异步

官方文档中给出

- (void)testDocumentOpening
{
    // 创建一个expectation对象
    XCTestExpectation *documentOpenExpectation = [self expectationWithDescription:@"document open"];

    NSURL *URL = [[NSBundle bundleForClass:[self class]]
                              URLForResource:@"TestDocument" withExtension:@"mydoc"];
    UIDocument *doc = [[UIDocument alloc] initWithFileURL:URL];
    [doc openWithCompletionHandler:^(BOOL success) {
        XCTAssert(success);

        // assert成功后 便会调用expectation的fulfill方法,来触发下面的handler
        [documentOpenExpectation fulfill];
    }];

    // 在case最后使用这一方法,此时单测程序会阻塞到这里;除非达到了超时时间(秒单位)或者是回调函数中调用了fulfill,单测程序才会结束
    // 若是超时情况,则认为case失败;若通过expectation的fulfill触发,则case通过
    [self waitForExpectationsWithTimeout:1 handler:^(NSError *error) {
        [doc closeWithCompletionHandler:nil];
    }];
}
  - (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:@"这里可以是操作出错的原因描述。。。

异步测试除了使用 expectationWithDescription以外,还可以使用 expectationForPredicateexpectationForNotification

下面这个例子使用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)
}

测试失败的调试工具

  • 断点调试

添加测试断点

![断点调试](http://ogpt2m9nl.bkt.clouddn.com/屏幕快照 2017-03-26 下午5.50.42.png)

当测试方法发布故障断言时,此断点停止测试运​​行。这样可以通过在测试代码中设置的断点停止执行,快速找到存在问题的位置。
![测试断点](http://ogpt2m9nl.bkt.clouddn.com/屏幕快照 2017-03-26 下午5.51.48.png)

使用命令行运行测试

使用Xcode命令行工具,你可以使用脚本自动构建和测试你的项目。

  • 例如:

测试本地OS X的MyApp
xcodebuild test -project MyAppProject.xcodeproj -scheme MyApp -destination 'platform=OS X,arch=x86_64'

模拟器上运行,使用模拟器可以应对不同的外形因素和操作系统版本:
例如 > xcodebuild test -project MyAppProject.xcodeproj -scheme MyApp -destination 'platform=iOS Simulator,name=iPhone,0S=7.0'

测试机上运行:
> xcodebuild test -project MyAppProject.xcodeproj -scheme MyApp -destination 'platform=iOS,name=Development test6s

destination参数可以连接在一起,一个命令在指定共享配置目标上执行测试。例如,下面的命令链将前面三个例子结合在一起变成一个命令:

xcodebuild test -project MyAppProject.xcodeproj -scheme MyApp-destination 'platform=OS X,arch=x86_64'-destination 'platform=iOS,name=Development iPod touch'-destination 'platform=Simulator,name=iPhone,OS=9.0'

如果测试失败,xcodebuild返回非零退出代码。

关于命令行运行测试,你需要知道一些基本要素。关于xcodebuild的详细信息,在终端应用窗口使用man:

man xcodebuild

辅助工具

![辅助编辑器](http://ogpt2m9nl.bkt.clouddn.com/屏幕快照 2017-03-26 下午6.06.43.png)

Test Callers category 。如果你刚刚修复导致应用测试失败的方法,也许你希望检查其他测试中调用的该方法是否运行成功。在源代码编辑器中有问题的方法中,打开assistant editor并从菜单中选择 Test Classes category。在弹窗菜单中可以导航到任何调用它的测试方法中,运行这些测试方法确保回归测试。

Test Classes category。它与Test Callers category.类似,但显示的是有测试方法的类的列表,可以导航到你正在编辑的类。

代码覆盖

代码覆盖率是Xcode7的功能,可以在视觉上看到和衡量你的代码测试覆盖率。有了代码覆盖率,你可以确定测试是否符合你的预期。
启用代码覆盖率

Xcode的代码覆盖率由LLVM支持的测试操作。当你启用代码覆盖率,LLVM基于方法和函数调用的频率来收集覆盖数据。代码覆盖率选项可以收集单元测试和UI测试正确性和性能数据,

编辑scheme的测试操作可以启用代码覆盖率。

  1. 在scheme编辑菜单中选择Edit Scheme。
    ![选择Edit Scheme](http://ogpt2m9nl.bkt.clouddn.com/屏幕快照 2017-03-26 下午6.12.41.png)
  2. 选择测试操作。
  3. 启用代码覆盖复选框以收集覆盖数据。
    ![启用代码覆盖](http://ogpt2m9nl.bkt.clouddn.com/屏幕快照 2017-03-26 下午6.13.52.png)

注意:收集代码覆盖率数据会导致性能损耗。无论损耗是否显著,它均会影响执行代码的线性方式,因此在测试运行中启用代码覆盖率,性能结果依然具有可比性。然而,当你正在认真评估测试程序性能时,你应该考虑是否启用代码覆盖率。

  • 代码覆盖率如何符合测试

代码覆盖率是用来衡量测试价值的工具。它回答了以下问题

当你运行测试时,什么代码真正运行?
多少测试才算足够?
换句话说,你是否设计足够的测试确保你所有的代码都检查了正确性和性能?
代码的哪部分没有被测试?
在测试运行完成后,Xcode采用LLVM覆盖数据并在报告导航中创建覆盖率报告,参见覆盖率面板。它显示了测试的摘要信息,源文件和源文件中的方法列表以及每个文件中的覆盖百分比。
![代码覆盖率](http://ogpt2m9nl.bkt.clouddn.com/屏幕快照 2017-03-26 下午6.15.54.png)

源代码编辑器展示了文件中代码的行数并高亮未执行的代码。它高亮需要覆盖的代码区域而非已经覆盖的区域。

例如,将指针放在-[Calculator input:]方法上,将显示一个按钮,索引到源代码。
![索引代码](http://ogpt2m9nl.bkt.clouddn.com/屏幕快照 2017-03-26 下午6.17.12.png)

覆盖注释在右边显示,显示了在测试中代码某个特定部分被执行的次数。例如:
![显示执行次数](http://ogpt2m9nl.bkt.clouddn.com/屏幕快照 2017-03-26 下午6.18.00.png)
input:方法,在测试中被频繁调用。然而,有部分方法并未被调用。在源代码编辑器中有明显的标记,如图:
![提示](http://ogpt2m9nl.bkt.clouddn.com/屏幕快照 2017-03-26 下午6.18.07.png)

官方文档:关于Xcode测试

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

推荐阅读更多精彩内容