今年苹果推出的iOS8和Swift的新功能让人兴奋。同时,苹果对于Xcode的测试工具的改进却也会影响深远。现在我们来看下XCTest,Xcode内置的测试框架。以及,Xcode6新增的XCTestExpectation和性能测试。
现在Xcode项目已经支持out-of-the-box的测试。比如,创建一个新的iOS应用项目后,项目会自动配置两个顶层的group:一个是“应用名称”的group,一个是“项目名称Test”group。对应于这两个顶层的group的是两个target。一个运行的target,一个测试的target。项目自动生成的scheme也允许用户用Command+R运行应用,Command+U编译运行测试的target。
在测试的target中,默认的情况下只有一个叫做“应用名称Test”的文件。这个文件里面会包含一个XCTextCase类,里面有对setUp方法和tearDown方法的调用,还有一个示例的测试方法和性能测试的test case。
XCTestCase
Xcode的单元测试包含在XCTestCase子类中。组织测试的时候需要尽量考虑实际的应用操作流程。
setUp & tearDown
setUp方法在XCTestCase的测试方法调用之前调用。当测试全部结束之后调用tearDown方法。
class MVVMTests: XCTestCase { override func setUp() { super.setUp() } override func tearDown() { // Put teardown code here. This method is called after the invocation of each test method in the class. super.tearDown() } }
setUp方法可以在测试之前创建在test case方法中需要用到的一些对象等。tearDown方法则在全部的test case执行结束之后清理测试现场,释放资源删除不用的对象等。所以,setUp方法一般都是这么用的:
var calendar: NSCalendar? var locale: NSLocale? override func setUp() { super.setUp() self.calendar = NSCalendar(identifier: NSGregorianCalendar) self.locale = NSLocale(localeIdentifier: "en_US") }
XCTestCase的初始化不是用户控制的,所以属性在setUp方法中初始化的属性只能被定义为optonal的。不定义成optional的话,就只能在定义属性的时候直接给出初始化。如:
var calendar: NSCalendar = NSCalendar(identifier: NSGregorianCalendar) var locale: NSLocale = NSLocale(localeIdentifier: "en_US")
功能测试
test case中的每一个方法都是test开头,这样容易辨识。方法中会执行断言(assertion),来判断这个测试是否通过。
func testExample() { XCTAssertEqual(1 + 1, 2, "one plus one equals two") }
常用的XCTest断言
XCTest会用到很多的断言,很多,但是只有一部分是常用到的。这里一一列出:
基本测试
所有的断言都是从最基本的这个断言演化出来的:
XCTAssert(expression, format...)
如果expression(表达式)执行的结果为true的话,这个测试通过。否则,测试失败,并在console中输出后面的format字符串。
后面基于XCTAssert演化出来的断言,不仅可以满足测试的需求而且可以更好更明确的表达出你要测试的是什么。最好是使用这些演化出来的断言,XCTestAssert不是必须最好不要用。
Bool测试
对于bool型的数据,或者只是简单的bool型的表达式,使用XCTestAssertTrue或者XCTestAssertFalse:
XCTAssertTrue(expression, format...)
XCTAssertFalse(expression, format...)
相等测试
测试两个值是否相等使用XCTAssert[Not]Equal:
XCTAssertEqual(expression1, expression2, format...)
XCTAssertNotEqual(expression1, expression2, format...)
XCTAssertGreaterThan[OrEqual] & XCTAssertLessThan[OrEqual], 和下面的条件操作符比较的是一个意思 == with >, >=, <, 以及 <=
在Double、Float型数据的对比中使用XCTAssert[Not]EqualWithAccuracy来处理浮点精度的问题:
XCTAssertEqualWithAccuracy(expression1, expression2, accuracy, format...)
XCTAssertNotEqualWithAccuracy(expression1, expression2, accuracy, format...)
Nil测试
使用XCTAssert[Not]Nil断言判断给定的表达式值是否为nil:
XCTAssertNil(expression, format...)
XCTAssertNotNil(expression, format...)
无条件失败断言
最后,XCTFail提供的是无条件断言:
XCTFail(format...)
XCTFail,无条件的都是测试失败。这个东东有什么用处呢。在测试驱动里有这么个情况,你定义了测试方法,但是没有给出具体的实现。那么你不会希望这个测试能通过的。是的,XCTFail就是这么个用途。一般被用作一个占位断言。等你的测试方法完善好了之后再换成最贴近你的测试的断言。有或者,在某些情况下else了之后就是不应该出现的情况。那么这个时候可以把XCTFail放在这个else里面。
性能测试
在Xcode6中新增的测试代码性能的功能:
func testPerformanceExample() { let dateFormatter = NSDateFormatter() dateFormatter.dateStyle = .LongStyle dateFormatter.timeStyle = .ShortStyle let date = NSDate() self.measureBlock() { let string = dateFormatter.stringFromDate(date) } }
测试结果:
Test Case '-[MVVMTests.MVVMTests testPerformanceExample]' started. <unknown>:0: Test Case '-[MVVMTests.MVVMTests testPerformanceExample]' measured [Time, seconds] average: 0.000, relative standard deviation: 257.209%, values: [0.000390, 0.000010, 0.000007, 0.000006, 0.000006, 0.000006, 0.000006, 0.000006, 0.000006, 0.000006], performanceMetricID:com.apple.XCTPerformanceMetric_WallClockTime, baselineName: "", baselineAverage: , maxPercentRegression: 10.000%, maxPercentRelativeStandardDeviation: 10.000%, maxRegression: 0.100, maxStandardDeviation: 0.100 Test Case '-[MVVMTests.MVVMTests testPerformanceExample]' passed (0.278 seconds).
性能测试可以帮助开发者建立一个主要功能的基本性能基线。确保这些主要的功能代码和算法能在这个性能基线内完成。
XCTestExpectation
Xcode单元测试中加入的最令人兴奋的功能也许就是类XCTestExpression类带入的异步测试了。现在测试可以等待指定长度的时间,一直到某些条件符合的时候在开始测试。而不用再写很多的GCD代码控制。
要使用异步测试,首先用方法expectationWithDescription创建一个expection。
let expectation = expectationWithDescription("...")
之后,在方法的最后添加方法waitForExpectationsWithTimeout,指定等待超时的时间和指定时间内条件无法满足时执行的closure。
waitForExpectationsWithTimeout(10) { (error) in // ... }
剩下的就是在异步测试剩下的回调函数中告诉expectation条件已经满足。
expectation.fulfill()
如果在测试中有多个expectation,则每个expectation都必须fulfill,否则测试不通过。
这里是一个测试异步网络访问的示例:
func testAsynchronousURLConnection(){ let URL = NSURL(string: "http://www.baidu.com")! let expectation = expectationWithDescription("GET \(URL)") let session = NSURLSession.sharedSession() let task = session.dataTaskWithURL(URL, completionHandler: {(data, response, error) in expectation.fulfill() // 告诉expectation满足测试了 XCTAssertNotNil(data, "返回数据不应该为空") XCTAssertNil(error, "error应该为nil") if response != nil { var httpResponse: NSHTTPURLResponse = response as NSHTTPURLResponse XCTAssertEqual(httpResponse.URL!.absoluteString!, URL, "HTTPResponse的URL应该和请求URL一致") XCTAssertEqual(httpResponse.statusCode, 200, "HTTPResponse状态码应该是200") XCTAssertEqual(httpResponse.MIMEType as String, "text/html", "HTTPResponse内容应该是text/html") } else{ XCTFail("返回内容不是NSHTTPURLResponse类型") } }) task.resume() waitForExpectationsWithTimeout(task.originalRequest.timeoutInterval, handler: {error in task.cancel() }) }
使用Mock
有了异步测试的支持,Xcode快要把满足测试驱动开发的龙珠已经集齐了。但是,还差一个Mock!
Mock是一个很有用的东西。使用这个技术可以有效的分离那些不利于测试的因素,比如:过得复杂、不确定、性能约束等。比如遇到交互的网络交互、高强度的数据库查询或者某些存在资源竞争的状态等。
有很多的开源库支持Mock和Stub。但是这些库都严重的依赖于Objective-C运行时,所以在Swift下某些功能无法使用。在Swift下,类可以定义在一个类的方法中。这一特点允许mock自包含(self-contain)的对象。只要定义一个mock类,然后override必要的方法:
func testFetchRequestWithMockedManagedObjectContext() { class MockNSManagedObjectContext: NSManagedObjectContext { private override func executeFetchRequest(request: NSFetchRequest, error: NSErrorPointer) -> [AnyObject]? { return [["name": "张三", "email": "zhangsan@apple.com"]] } } let mockContext = MockNSManagedObjectContext() let fetchRequest = NSFetchRequest(entityName: "User") fetchRequest.predicate = NSPredicate(format: "email ENDSWITH[cd] %@", "apple.com") fetchRequest.resultType = NSFetchRequestResultType.DictionaryResultType var error: NSError? let results = mockContext.executeFetchRequest(fetchRequest, error: &error) XCTAssertNil(error, "error应该为nil") XCTAssertEqual(results!.count, 1, "fetch request应该只返回一个结构") let result = results![0] as [String: String] XCTAssertEqual(result["name"]! as String, "张三", "name应该是张三") XCTAssertEqual(result["email"]! as String, "zhangsan@apple.com", "email应该是zhangsan@apple.com") }
结论
Xcode6的内置工具终于足够的好了。也就是说即使是很大的APP也没有必要为了单元测试的代码覆盖率而排斥Xcode内置的测试工具。无论什么样的测试,XCTest的各种断言、expectation和性能测试都足够应对。但是无论多好的工具,都需要用好才行。
如果你在测试iOS或者OS X的APP,开始为自动添加的测试类添加一些断言并按下Command+U。你一定会发现感觉这些工具让你的测试方便不少!
示例代码在这里。
参考文章:http://nshipster.com/xctestcase/