> 原文出处: http://chengway.in/post/ji-zhu/core-data-by-tutorials-bi-ji-liu
今天我们来关注一下CoreData的单元测试,其实在写程序之前,先写测试,将问题一点点分解,也是TDD所倡导的做法,这也是我今年所期望达成的一个目标,新开项目按TDD的流程来做,以后也会整理些这方面的东西。如果你对CoreData的其他方面感兴趣请查看我之前的笔记或直接购买[《Core Data by Tutorials》](http://www.raywenderlich.com/store/core-data-by-tutorials)
## **Chapter 8: Unit Testing**
作者列举了一系列单元测试的好处:帮助你在一开始就组织好项目的结构,可以不用操心UI去测试核心功能。单元测试还可以方便重构。还可以更好地拆分UI进行测试。
这章主要焦距在XCTest这个框架来测试Core Data程序,多数情况下Core Data的测试要依赖于真实的Core Data stack,但又不想将单元测试的test data与你手动添加的接口测试弄混,本章也提供了解决方案。
### **一、Getting started**
本章要测试的是一个关于野营管理的APP,主要管理营地、预订(包括时间表和付款情况)。作者将整个业务流程分解为三块:
1. campsites(野营地)
2. campers(野营者)
3. reservations(预订)
由于swift内部的访问控制,app和其test分别属于不同的targets和不同的modules,因此你并不能普通地从tests中访问app中的classes,这里有两个解决办法:
1. 把App中的classes和methods标记为**public**,是其对tests可见。
2. 直接在test target里添加所需要的classes。
作者提供的实例中,已经将要测试的类和方法标记为*public*的了,现在就可以对Core Data部分进行测试了,作者在测试开始前给了一些建议:
> Good unit tests follow the acronym **FIRST**:
>
> • **F**ast: If your tests take too long to run, you won’t bother running them.
>
> • **I**solated: Any test should function properly when run on its own or before or after any other test.
>
> • **R**epeatable: You should get the same results every time you run the test against the same codebase.
>
> • **S**elf-verifying: The test itself should report success or failure; you shouldn’t have to check the contents of a file or a console log.
>
> • **T**imely: There’s some benefit to writing the tests after you’ve already written the code, particularly if you’re writing a new test to cover a new bug. Ideally, though, the tests come first to act as a specification for the functionality you’re developing.
为了达到上面提到“**FIRST**”目标,我们需要修改Core Data stack使用**in-memory store**而不是SQLite-backed store。具体的做法是为**test target**创建一个CoreDataStack的子类来修改*store type*。
~~~
class TestCoreDataStack: CoreDataStack {
override init() {
super.init()
self.persistentStoreCoordinator = {
var psc: NSPersistentStoreCoordinator? = NSPersistentStoreCoordinator(managedObjectModel:
self.managedObjectModel)
var error: NSError? = nil
var ps = psc!.addPersistentStoreWithType(
NSInMemoryStoreType, configuration: nil,
URL: nil, options: nil, error: &error)
if (ps == nil) {
abort()
}
return psc
}()
}
}
~~~
### **二、Your first test**
单元测试需要将APP的逻辑拆分出来,我们创建一个类来封装这些逻辑。作者这里创建的第一个测试类为**CamperServiceTests**是**XCTestCase**的子类,用来测试APP**CamperService**类中的逻辑
~~~
import UIKit
import XCTest
import CoreData
import CampgroundManager
//
class CamperServiceTests: XCTestCase {
var coreDataStack: CoreDataStack!
var camperService: CamperService!
override func setUp() {
super.setUp()
coreDataStack = TestCoreDataStack()
camperService = CamperService(managedObjectContext: coreDataStack.mainContext!, coreDataStack: coreDataStack)
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
coreDataStack = nil
camperService = nil
}
func testAddCamper() {
let camper = camperService.addCamper("Bacon Lover", phoneNumber: "910-543-9000")
XCTAssertNotNil(camper, "Camper should not nil")
XCTAssertTrue(camper?.fullName == "Bacon Lover")
XCTAssertTrue(camper?.phoneNumber == "910-543-9000")
}
~~~
**setUp**会在每次测试前被调用,这里可以创建一些测试需要用到东西,而且因为使用的是in-memory store,每次在setUp中创建的context都是全新的。**tearDown**相对于*setUp*,是在每次test结束后调用,用来清除一些属性。上面的例子主要测试了addCamper()方法。
~~~
这里注意的就是该测试创建的对象和属性都不会保存在任何store中的。
~~~
### **三、Asynchronous tests**
关于异步测试,这里用到了两个context,一个root context运行在后台线程中,另外一个main context是root context的子类,让context分别在正确的线程中执行其实也很简单,主要采用下面两种方法:
1. performBlockAndWait() 将等待block里的内容执行完后才继续
2. performBlock() 执行到此方法立即返回
测试第二种performBlock()方法时可能会需要些技巧,因为数据可能不会立即得到,还好XCTestCase提供了一个叫**expectations**的新特性。下面展示了使用**expectation**来完成对异步方法的测试:
~~~
let expectation = self.expectationWithDescription("Done!");
someService.callMethodWithCompletionHandler() {
expectation.fulfill()
}
self.waitForExpectationsWithTimeout(2.0, handler: nil)
~~~
该特性的关键是要么是*expectation.fulfill()*被执行,要么触发超时产生一个异常expectation,这样test才能继续。
我们现在来为CamperServiceTests继续增加一个新的方法来测试root context的保存:
~~~
func testRootContextIsSavedAfterAddingCamper() {
//1 创建了一个针对异步测试的方法,主要是通过观察save方法触发的通知,触发通知后具体的handle返回一个true。
let expectRoot = self.expectationForNotification(
NSManagedObjectContextDidSaveNotification,
object: coreDataStack.rootContext) {
notification in
return true
}
//2 增加一个camper
let camper = camperService.addCamper("Bacon Lover",
phoneNumber: "910-543-9000")
//3 等待2秒,如果第1步没有return true,那么就触发error
self.waitForExpectationsWithTimeout(2.0) {
error in
XCTAssertNil(error, "Save did not occur")
}
}
~~~
### **四、Tests first**
这一节新建了一个**CampSiteServiceTests** Class 对CampSiteService进行测试,具体code形式与上一节类似,添加了测试**testAddCampSite()**和**testRootContextIsSavedAfterAddingCampsite()**,作者在这里主要展示了**TDD**的概念。
> Test-Driven Development (TDD) is a way of developing an application by writing a test first, then incrementally implementing the feature until the test passes. The code is then refactored for the next feature or improvement.
根据需求又写了一个**testGetCampSiteWithMatchingSiteNumber()**方法用来测试**getCampSite()**,因为campSiteService.addCampSite()方法在之前的测试方法中已经通过测试了,所以这里可以放心去用,这就是TDD的一个精髓吧。
~~~
func testGetCampSiteWithMatchingSiteNumber(){
campSiteService.addCampSite(1, electricity: true,
water: true)
let campSite = campSiteService.getCampSite(1)
XCTAssertNotNil(campSite, "A campsite should be returned")
}
func testGetCampSiteNoMatchingSiteNumber(){
campSiteService.addCampSite(1, electricity: true,
water: true)
let campSite = campSiteService.getCampSite(2)
XCTAssertNil(campSite, "No campsite should be returned")
}
~~~
写完测试方法运行一下CMD+U,当然通不过啦,我们还没有实现他。现在为CampSiteService类添加一个getCampSite()方法:
~~~
public func getCampSite(siteNumber: NSNumber) -> CampSite? {
let fetchRequest = NSFetchRequest(entityName: "CampSite") fetchRequest.predicate = NSPredicate(
format: "siteNumber == %@", argumentArray: [siteNumber])
var error: NSError?
let results = self.managedObjectContext.executeFetchRequest(
fetchRequest, error: &error)
if error != nil || results == nil {
return nil
}
return results!.first as CampSite?
}
~~~
现在重新CMD+U一下,就通过了。
### **五、Validation and refactoring**
最后一节主要针对APP中的**ReservationService**类进行测试,同样的是创建一个**ReservationServiceTests**测试类,这个test类的setUP和tearDown与第三节类似。只不过多了campSiteService与camperService的设置。在*testReserveCampSitePositiveNumberOfDays()*方法中对ReservationService类里的**reserveCampSite()**进行测试后,发现没有对numberOfNights有效性进行判断,随后进行了修改,这也算是展示了单元测试的另一种能力。作者是这么解释的:不管你对这些要测试的code有何了解,你尽肯能地针对这些API写一些测试,如果OK,那么皆大欢喜,如果出问题了,那意味着要么改进code要么改进测试代码。