苹果在2013年推出了一款叫做XCTest的Xcode测试框架,实在是喜闻乐见。由于旧有的测试框架更新停滞数年,一些第三方测试工具和测试框架争相提供了许多新功能和新特性。这次XCode中内置XCTest的决策让开发者们重拾旧爱,并且苹果今年还在Xcode 6中添加了此前漏掉的几项功能,这当中的异步测试功能更是让我欣喜异常。
如果我们的测试项目要执行一个异步任务,它可能会跑在其它的线程里,也可能会跑在主线程的RunLoop里,在这种时候我们应该如何去进行测试呢?
假如现在有一个web请求的功能需要测试。我们会开始web请求然后进入阻塞,然后随便在程序完成的代码块中做一个测试断言。然而,鉴于没有web请求的情况下更谈不上响应,为了调用程序完成部分的代码我们就需要在断言之前确保测试方法已经在执行。
下面来看一个下载web页面类的测试。在通常情况下我们不会真的去在测试时候去做web响应,而是用一些工具来中断响应(比如我偏爱的OHHTTPStubs)。不过在下面的例子里面我们需要更改一下规则——真正的完成一个web响应来进行测试。
我们使用了一个URL和一个程序完成块来对这个类进行测试,它会下载URL所指的页面并调用这个程序块,如果成功会得到一个包含web页面的字串,而发生错误的时候则是一个空字符串。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
- (void)testCodeYouShouldNeverWrite
{
__block NSString *pageContents = nil;
[self.pageLoader requestUrl:@
"http://bignerdranch.com"
completionHandler:^(NSString *page) {
NSLog(@
"The web page is %ld bytes long."
, page.length);
pageContents = page;
// Test method ends before this test assertion is called
XCTAssert(pageContents.length > 0);
}];
// Nothing prevents the test method from returning before
// completionHandler is called.
}
|
在Xcode 6之前的版本里面并没有内置XCTest,想使用Xcode测试的只能是在主线程的RunLoop里面使用一个while循环,然后一直等待响应或者直到timeout,下面就是这种老旧方法的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
- (void)testAsyncTheOldWay
{
NSDate *timeoutDate = [NSDate dateWithTimeIntervalSinceNow:5.0];
__block BOOL responseHasArrived = NO;
[self.pageLoader requestUrl:@
"http://bignerdranch.com"
completionHandler:^(NSString *page) {
NSLog(@
"The web page is %ld bytes long."
, page.length);
responseHasArrived = YES;
XCTAssert(page.length > 0);
}];
while
(responseHasArrived == NO && ([timeoutDate timeIntervalSinceNow] > 0)) {
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.01, YES);
}
if
(responseHasArrived == NO) {
XCTFail(@
"Test timed out"
);
}
}
|
这个while循环在主线程里面每隔10毫秒会跑一次,直到有响应或者5秒之后超出响应时间限制才会跳出。这个方法十分有效而且看上去也不糟,但这并不意味着开发的终结——这个方法还是不够好。
接下来说一个更加优化的办法。
高期望(High Expectations)
在Xcode 6里,苹果以XCTestExpection类的方式向XCTest框架里添加了测试期望(test expection)。当我们实例化一个测试期望(XCTestExpectation)的时候,测试框架就会预计它在之后的某一时刻被实现。最终的程序完成代码块中的测试代码会调用XCTestExpection类中的fulfill方法来实现期望。这一方法替代了我们之前例子里面使用responseHasArrived作为Flag的方式,这时我们让测试框架等待(有时限)测试期望通过XCTestCase的waitForExpectationsWithTimeout:handler:方法实现。如果完成处理的代码在指定时限里执行并调用了fulfill方法,那么就说明所有的测试期望在此期间都已经被实现。否则,这个测试就悲剧了,它会默默的存在程序中而不会被实现哪怕一次……
当然,失败结果并不意味着失败的测试,只有不明就里的测试结果才算失败的测试。
下面是使用XCTestExpection的示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
(void)testWebPageDownload
{
XCTestExpectation *expectation =
[self expectationWithDescription:@
"High Expectations"
];
[self.pageLoader requestUrl:@
"http://bignerdranch.com"
completionHandler:^(NSString *page) {
NSLog(@
"The web page is %ld bytes long."
, page.length);
XCTAssert(page.length > 0);
[expectation fulfill];
}];
[self waitForExpectationsWithTimeout:5.0 handler:^(NSError *error) {
if
(error) {
NSLog(@
"Timeout Error: %@"
, error);
}
}];
}
|
在测试期望里面输出一些信息以便增强测试结果的可读性。在最后的代码段里面使用[expectation fulfill]来告知此次测试所期望的部分已经确切实现过了。然后用waitForExpectationsWithTimeout:handler方法等待响应,这段会在接受响应之后执行……或者超时之后也会执行。
在OC中实现效果不错,此外我们在Swift下同样也可以实现这个测试:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
func testWebPageDownload() {
let expectation = expectationWithDescription(
"Swift Expectations"
)
self.pageLoader.requestUrl(
"http://bignerdranch.com"
, completion: {
(page: String?) -> ()
in
if
let downloadedPage = page {
XCTAssert(!downloadedPage.isEmpty,
"The page is empty"
)
expectation.fulfill()
}
})
waitForExpectationsWithTimeout(5.0, handler:nil)
}
|