Test

单元测试

单元是应用的最小可测试部件。单元测试(又称为模块测试,Unit Testing)是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。如果代码之间的耦合度很小,那么可以将其分解成多个小单元,从而更易于测试。TDD(Test-Driven Development测试驱动开发)要求开发者在编写功能前先将其测试代码写好,然后边编写实现代码边进行测试,从而保证实现尽早发现并修改问题,因此整个项目将由测试来驱动,有助于开发出高质量的代码。

黑盒测试

黑盒测试是一种测试方法,盒子是指被测试的软件,黑是指盒子是不可视,测试者不清楚也不关心盒子内部的构造以及运作。白盒测试又称功能测试,它是以用户的角度,对外部进行测试,只检查功能是否能够按照需求正常使用。

白盒测试

白盒测试是一种测试方法,盒子是指被测试的软件,白是指盒子是可视的,测试者清楚盒子内部是如何构造以及如何运作。白盒测试又称结构测试,它是以实现者的角度,对内部进行测试,需检查功能是否能够按照需求正常实现。

懵懂认知

刚工作时,我以为测试只是测试干的事,开发只要做完自己的工作,交给测试去测就行。后来发现,从前面定义来看,测试干的是黑盒测试。对于开发而言,为什么要进行白盒测试呢?

  • 黑盒测试测出的问题归根结底还是开发实现的问题。测试的确不清楚功能是如何实现的,也不想知道。白盒测试也只能由清楚实现的细节开发来完成,可以针对具体的功能实现进行细致全面地测试。
  • 黑盒测试一般由人工操作,会因为人的习惯、偏好、心情、态度而存在耗时耗力、范围局限、路径随意、深浅不均等等问题。而白盒测试一般都有工具辅助,只要了解细节的开发编写好全面的测试用例,就可以快速全面专注平均地测试。
  • 一般项目需要进行到一定程度,能够出现可测试的样品时,才可以进行黑盒测试。也就是说,问题的暴露从开发阶段推迟到测试阶段才被发现。而白盒测试可以脱离软件整体真对单独模块(比如framework、library、class、methods)在开发阶段就提前进行。
  • 黑盒测试需要有软件样品,对于每一次修改了测出的问题后,构建生成新的软件样品再进行黑盒测试不太现实。而脱离软件整体的白盒测试可以独立地对问题所涉及到的地方修改后进行多次快速地测试。
  • 接口设计好后,即接口功能、接口名称、参数个数、参数类型、返回类型都明确稳定后,白盒测试就可以开始编写测试用例并测试。这有助于实现在早期就能发现问题,为软件提供稳定健壮的组件。甚至对于实现的内部有任何改变,只要有一套完整的白盒测试用例,都可以快速检查是否修复旧问题是否产生新问题。

所以,无论是自身充满好奇心,还是对自己严格要求,即使是测试,都会有助于培养良好习惯和提高编码水平。

Xcode测试

由于Xcode5以后内嵌了XCTest(兼容iOS7+和OS X10.8+)这个测试框架,可以很方便地进行测试,且能满足一般测试需求,所以OCUnit和SenTest我目前就不再深入研究。

Xcode里,目标(Target)通过构建(Build)生成产品(Production)。测试目标构建后会生成测试包。如果测试过程需要使用不同类型的数据(文件图片音频视频等),可以把它们添加到测试包中,并在测试方法里使用[NSBundle bundleForClass:测试类],在运行时获取正确的测试包,便可以取得正确的数据。

快捷键Command+U是起动当前Scheme配置的测试包的测试任务。Scheme(方案)控制着构建(Build)、运行(Run)、测试(Test)、调试(Profile)、分析(Analyse)和归档(Archive)这些命令的行为。其中,可以通过添加或删除、启用或关闭,来控制测试范围。可以在测试导航栏找到所有的测试包、测试类、测试方法,可以分不同抽象级别进行测试。也可以在源文件测试整个类或者某个方法。

XCTestCase

每一个创建的测试类都是基于XCTestCase的子类型。对于每个测试类,测试方法都是以test开头、没有参数、返回void、用于测试不同目的的实例方法。对于每个测试方法,其通用代码(比如打开关闭文件或者网络)可以写在setUp(测试开始前)和tearDown(测试结束后)里面。执行测试时,一个测试类会创建对应测试方法数量的该类实例,每个实例在setup后执行一个对应的测试方法接着tearDown。这样连续重复执行所有的测试方法,测完一个类,接着下个类。一直重复直到测完所有测试类的所有测试方法。

XCTest断言

测试方法中,对测试的表达式进行预期判断。只有不符合预期判断的表达式才算是测试失败。XCTest提供了很多内建的预判断言。

无条件报错

  • XCTFail

等价测试

  • XCTAssertEqualObjects
  • XCTAssertNotEqualObjects
  • XCTAssertEqual
  • XCTAssertNotEqual
  • XCTAssertEqualWithAccuracy
  • XCTAssertNotEqualWithAccuracy

nil测试

  • XCTAssertNil
  • XCTAssertNotNil

布尔测试

  • XCTAssertTrue
  • XCTAssert(与XCTAssertTrue同义)
  • XCTAssertFalse

异常测试

  • XCTAssertThrows
  • XCTAssertThrowsSpecific
  • XCTAssertThrowsSpecificNamed
  • XCTAssertNoThrow
  • XCTAssertNoThrowSpecific
  • XCTAssertNoThrowSpecificNamed(对AppKit和Foundation非常有用)

断点

在断点导航栏中,通过Add Test Failure Breakpoint添加一个测试失败断点。当测试方法失败触发了失败端点时,这个断点会终止测试的执行,并停止在测试代码发生错误点,这可以快速找到问题发生的位置。测试时异常断点捕获异常也会终止测试执行,所以测试运行时通常会关闭异常断点以避免不适当的定位。当然,寻找一个特定问题并想终止测试来修复它时,也可以打开异常断点恢复捕获。

性能测试

在Xcode6以后,Apple提供了内建的measureBlock以确定一个性能基准。可以使用它们,方便地在测试用例中确认重要的算法以及随着时间的推移程序保持高性能。

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

异步测试

之前在介绍GCD的时候,曾经提到过可以使用GCD实现异步测试:

- (void)testAsyncWithGroup {
    dispatch_group_t group = dispatch_group_create();
    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);
    });
    XCTAssertEqual(0,         dispatch_group_wait(group,         dispatch_time(DISPATCH_TIME_NOW, 2         * NSEC_PER_SEC)));
}
- (void)testAsyncWithSemaphore {
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        if (!dispatch_semaphore_wait(semaphore, DISPATCH_TIME_NOW)) {
            sleep(1);
            dispatch_semaphore_signal(semaphore);
        }
    });
    long value = dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC));
    if (!value) {
        dispatch_semaphore_signal(semaphore);
    }
    XCTAssertEqual(0, value);
}

在Xcode6以后,Apple提供了内建的XCTestExpectation(期望)以支持异步测试。首先使用expectationWithDescription:创建一个期望;然后在调用异步方法后使用waitForExpectationsWithTimeout,指定一个超时回调函数,如果在规定的时间内期望没有得到实现便会执行回调函数;最后在异步方法里使用fulfill完成实现。注意,如果一个测试用例里有多个期望,只有所有的期望都实现了才算等待成功。

- (void)testDocumentOpening {
    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);
        [documentOpenExpectation fulfill];
    }];
    [self waitForExpectationsWithTimeout:1 handler:^(NSError *error) {
        [doc closeWithCompletionHandler:nil];
    }];
}

编码指南

通过测试达到的稳定水平取决于编写测试的质量,而编写测试的难易程度也取决于编码方式。为测试设计的编码方式有助于减小编写难度,写出良好测试。

  • 定义API:每个API是为了满足对应的需求而设计的。越早定义好API形成明确稳定的标准,就越好写测试代码,即使没有实现API。只要API的定义不变,实现有了变动也可以使用相同的测试代码进行测试。API的定义包括功能描述、输入范围、输出范围,异常抛出,使用条件和返回类型。
  • 边写边测:每当设计和编写一个方法时,就可以编写对应的测试用例来确保API的正确性。因为此时是对该API最熟悉的时段,随着时间的流逝,回过头来重新为很久前写过的实现编写测试用例,会有理解偏差的风险和重新学习的成本。
  • 测试边界:对超越方法输入范围外的输入值进行测试。
  • 测试出错:对能使方法出错的条件进行测试,确保方法正面响应错误,比如抛出异常或返回错误代码。
  • 测试全面:尽量编写全面复杂的测试代码(比如多线程环境),测试实现在各个方面的表现,发现潜在问题。
  • 测试Bug:每当修复完一个Bug,都需要重复对应的测试,来验证此次修复的有效性。

命令行测试

命令行工具xcodebuild的test可以进行测试,参数-destination指定不同的测试目的。

在64位OSX测试:xcodebuild test -project MyAppProject.xcodeproj -scheme MyApp -destination ‘platform=OS X,arch=x86_64’

在一个iPod测试:xcodebuild test -project MyAppProject.xcodeproj -scheme MyApp -destination ‘platform=iOS,name=iPod Name’

在iOS模拟器测试:xcodebuild test -project MyAppProject.xcodeproj -scheme MyApp -destination ‘platform=iOS Simulator,name=iPhone,0S=7.0’

-destination参数可以被连接在一起,这样就可以多个目标共享同一个方案(scheme):xcodebuild test -project MyAppProject.xcodeproj -scheme MyApp

-destination ‘platform=OS X,arch=x86_64’

-destination ‘platform=iOS,name=iPod Name’

-destination ‘platform=iOS Simulator,name=iPhone,0S=7.0’

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


延伸阅读

Testing with Xcode

Xcode 6单元测试


Test
https://hllovesgithub.github.io/2015/12/06/2015-11-29-Test/
作者
Hu Liang
发布于
2015年12月6日
许可协议