博文翻译之单元测试
首先附上原文链接
纯属练手,如果错误,还请不吝赐教
iOS8和swift在2014年的WWDC上出尽风头,导致Xcode6中测试的改进在短时间内可能不会引起太大重视。
本周我们就来看看XCTest——一个内置进Xcode的测试框架,还有Xcode6中新加的XCTestExpectation和性能测试。
现在大部分的Xcode工程都支持“盒外”测试。比如用快捷键⇧⌘N新建一个iOS app,你会发现新建的工程不仅有你的工程文件“AppName”,还有“AppNameTests”,如图
工程自动生成的scheme可以用快捷键⌘R直接编译、executable target,用⌘U编译、运行测试target(test target)。
在测试target(test target)中只有一个文件,本例中就是TEST2Tests.m文件,点进去你可以发现TEST2Tests类是XCTestCase的子类。TEST2Tests包括setUp和tearDown方法,还有方法测试、性能测试的示例。
XCTestCase
用XCTestCase子类进行Xcode单元测试。每个XCTestCase子类都会进行某一类的测试,比如按照功能划分一个app的测试,那么每个功能都会有一个专门的XCTestCase。
注:用数量有限的测试用例逻辑性的分割测试,这种策略在我们开发的软件依靠的系统变化时会发挥巨大的作用。(比如iOS9.0升级到iOS10,就是系统变化)
setUp & tearDown
setUp
is called before each test in an XCTestCase
is run, and when that test finishes running, tearDown
is called:
XCTestCase中每一个测试执行前都会执行setUp,当测试执行完成后会执行tearDown方法。
@interface Tests : XCTestCase @property NSCalendar *calendar; @property NSLocale *locale; @end @implementation Tests - (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]; } @end
这些方法可以用来构造XCTestCase中所有测试方法都用的对象,如下
- (void)setUp { [super setUp]; self.calendar = [NSCalendar calendarWithIdentifier:NSCalendarIdentifierGregorian]; self.locale = [NSLocale localeWithLocaleIdentifier:@"en_US"]; }
(此段仅适用于swift)因为XCTestCase通常不会在XCTestCase定义中初始化,所以swift中,在setUp方法中初始化的公共属性会修饰为可选变量。直接赋值比在setUp中初始化变量更方便:
Since XCTestCase is not intended to be initialized directly from within a test case definition, shared properties initialized in setUp are declared as optional vars in Swift. As such, it’s often much simpler to forgo setUp and assign default values instead:
var calendar: NSCalendar = NSCalendar(identifier: NSGregorianCalendar) var locale: NSLocale = NSLocale(localeIdentifier: "en_US")
测试方法(Functional Testing)
XCTestCase(test case)中每个用“test”开头的方法都会被系统识别成一个测试方法,并且系统会执行此方法中的每个断言,根据断言来判断测试通过与否。
For example, the function testOnePlusOneEqualsTwo
will pass if 1 + 1
is equal to 2
:
举个例子,当1+1 = 2时,下面的testOnePlusOneEqualsTwo测试方法会通过:
- (void)testOnePlusOneEqualsTwo { XCTAssertEqual(1 + 1, 2, "one plus one should equal two"); }
断言(All of the XCTest Assertions You Really Need To Know)
XCTest(苹果封装的测试库,XCTestCase是测试类的父类)有自带的断言,我们可以把这些自带的断言归类成:
1.Fundamental Test
2.Boolean Tests
3.Equality Tests
4.Nil Tests
5.Unconditional Failure
6.Performance Testing
下面我们来逐个举例说明:
Fundamental Test
所有XCTest库中断言都来自一个基断言(姑且叫它基断言吧):
XCTAssert(expression, format...);
如果expression返回结果是true,测试通过,否则测试失败,打印出format信息。
虽然我们用XCTAssert可以满足基本需求,但接来下断言辅助会提供更多有用信息来帮助我们认识到我们到底在测试什么。可能的话,用能用到的最专业(特别、特殊)的断言。
Boolean Tests
当要判断的expression是布尔型变量或布尔型计算,用XCTAssertTrue
& XCTAssertFalse,如下:
XCTAssertTrue(expression, format...);
XCTAssertFalse(expression, format...);
注:XCTAssert等同于XCTAssertTrue
Equality Tests
当测试两个值是否相等时用XCTAssertEqual(XCTAssertNotEqual),XCTAssertEqual(XCTAssertNotEqual)用来测试值的数量值是否相等,XCTAssert[Not]EqualObjects用来测试两个对象是否相等。
XCTAssertEqual(expression1, expression2, format...);
XCTAssertNotEqual(expression1, expression2, format...);
XCTAssertEqualObjects(expression1, expression2, format...);
XCTAssertNotEqualObjects(expression1, expression2, format...);
XCTAssert[Not]EqualObjects在swift中是鸡肋,因为swift中数量值和对象没有区别。
此外还有XCTAssertGreaterThan[OrEqual] & XCTAssertLessThan[OrEqual]可以用来测试>, >=, <, <=几种情况。
当测试两个double类型或者float或者其他浮点型值是否相等时,用XCTAssert[Not]EqualWithAccuracy
XCTAssertEqualWithAccuracy(expression1, expression2, accuracy, format...);
XCTAssertNotEqualWithAccuracy(expression1, expression2, accuracy, format...);
Nil Tests
XCTAssert[Not]Nil用来测试一个值存在与否。
XCTAssertNil(expression, format...);
XCTAssertNotNil(expression, format...);
Unconditional Failure
总是失败的XCTFail断言~
XCTFail(format...);
XCTFail通常用来表示不应该执行到这个地方。它也可以用于处理已被其他流控制结构处理过的错误。(比如if判断语句不成功后,在else中放置XCTFail断言,令else下的语句总是失败)
Performance Testing
Xcode6中的新特性,衡量一段代码的性能。
- (void)testDateFormatterPerformance { NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; dateFormatter.dateStyle = NSDateFormatterLongStyle; dateFormatter.timeStyle = NSDateFormatterShortStyle; NSDate *date = [NSDate date]; [self measureBlock:^{ NSString *string = [dateFormatter stringFromDate:date]; }]; }
measureBlock会被执行十次,测试输出展示了平均执行时间和执行时间的标准偏差。如下图所示
/Users/LL/Desktop/lu/TEST2/TEST2Tests/TEST2Tests.m:37: Test Case '-[TEST2Tests testPerformanceExample]' measured [Time, seconds] average: 0.288, relative standard deviation: 37.778%, values: [0.212114, 0.295884, 0.228346, 0.181476, 0.163309, 0.189228, 0.420532, 0.493367, 0.293690, 0.398990], performanceMetricID:com.apple.XCTPerformanceMetric_WallClockTime, baselineName: "", baselineAverage: , maxPercentRegression: 10.000%, maxPercentRelativeStandardDeviation: 10.000%, maxRegression: 0.100, maxStandardDeviation: 0.100
性能测试可以测试经常用到的代码段在每台设备上最差表现,如果执行时间明显慢了,则测试失败。好的程序员应该测试所有重要的算法和流程,保证它们不会随着时间的流逝变的很慢。
XCTestExpectation
xcode6新特色中最激动人心的莫过于异步测试。以后再也不用求助于鬼画符一样的GCD,使用XCTestExpectation,你可以指定时长指定条件。
使用XCTestExpectation的第一步:用expectationWithDescription创建一个expectation,如下
XCTestExpectation *expectation = [self expectationWithDescription:@"..."];
然后调用waitForExpectationsWithTimeout方法,指定timeout时长,handler block会在测试条件满足或超时时调用,handler block是可选的。
[self waitForExpectationsWithTimeout:10 handler:^(NSError *error) { // ... }];
最后,在被测试的异步回调的方法中调用fulfill方法:
[expectation fulfill];
小贴士:必须在异步回调的结尾调用fulfill方法,这样当执行loop在执行完测试前退出,早点满足测试条件可以产生竞争状态(race condition)。如果测试有多个条件,只有当每个条件都执行了fulfill方法,并且在waitForExpectationsWithTimeout方法指定的时长内,测试才算通过。
下面举个用XCTestExpectation测试异步网络请求的例子:
- (void)testAsynchronousURLConnection { NSURL *URL = [NSURL URLWithString:@"http://nshipster.com/"]; NSString *description = [NSString stringWithFormat:@"GET %@", URL]; XCTestExpectation *expectation = [self expectationWithDescription:description]; NSURLSession *session = [NSURLSession sharedSession]; NSURLSessionDataTask *task = [session dataTaskWithURL:URL completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { XCTAssertNotNil(data, "data should not be nil"); XCTAssertNil(error, "error should be nil"); if ([response isKindOfClass:[NSHTTPURLResponse class]]) { NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; XCTAssertEqual(httpResponse.statusCode, 200, @"HTTP response status code should be 200"); XCTAssertEqualObjects(httpResponse.URL.absoluteString, URL.absoluteString, @"HTTP response URL should be equal to original URL"); XCTAssertEqualObjects(httpResponse.MIMEType, @"text/html", @"HTTP response content type should be text/html"); } else { XCTFail(@"Response was not NSHTTPURLResponse"); } [expectation fulfill]; }]; [task resume]; [self waitForExpectationsWithTimeout:task.originalRequest.timeoutInterval handler:^(NSError *error) { if (error != nil) { NSLog(@"Error: %@", error.localizedDescription); } [task cancel]; }]; }