原文出處: http://chengway.in/post/ji-zhu/core-data-by-tutorials-bi-ji-liu
今天我們來關注一下CoreData的單元測試,其實在寫程序之前,先寫測試,將問題一點點分解,也是TDD所倡導的做法,這也是我今年所期望達成的一個目標,新開項目按TDD的流程來做,以后也會整理些這方面的東西。如果你對CoreData的其他方面感興趣請查看我之前的筆記或直接購買《Core Data by Tutorials》
作者列舉了一系列單元測試的好處:幫助你在一開始就組織好項目的結(jié)構(gòu),可以不用操心UI去測試核心功能。單元測試還可以方便重構(gòu)。還可以更好地拆分UI進行測試。
這章主要焦距在XCTest這個框架來測試Core Data程序,多數(shù)情況下Core Data的測試要依賴于真實的Core Data stack,但又不想將單元測試的test data與你手動添加的接口測試弄混,本章也提供了解決方案。
本章要測試的是一個關于野營管理的APP,主要管理營地、預訂(包括時間表和付款情況)。作者將整個業(yè)務流程分解為三塊:
由于swift內(nèi)部的訪問控制,app和其test分別屬于不同的targets和不同的modules,因此你并不能普通地從tests中訪問app中的classes,這里有兩個解決辦法:
作者提供的實例中,已經(jīng)將要測試的類和方法標記為public的了,現(xiàn)在就可以對Core Data部分進行測試了,作者在測試開始前給了一些建議:
Good unit tests follow the acronym?FIRST:
??Fast: If your tests take too long to run, you won’t bother running them.
??Isolated: Any test should function properly when run on its own or before or after any other test.
??Repeatable: You should get the same results every time you run the test against the same codebase.
??Self-verifying: The test itself should report success or failure; you shouldn’t have to check the contents of a file or a console log.
??Timely: 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創(chuàng)建一個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
}()
}
}
單元測試需要將APP的邏輯拆分出來,我們創(chuàng)建一個類來封裝這些邏輯。作者這里創(chuàng)建的第一個測試類為CamperServiceTests是XCTestCase的子類,用來測試APPCamperService類中的邏輯
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會在每次測試前被調(diào)用,這里可以創(chuàng)建一些測試需要用到東西,而且因為使用的是in-memory store,每次在setUp中創(chuàng)建的context都是全新的。tearDown相對于setUp,是在每次test結(jié)束后調(diào)用,用來清除一些屬性。上面的例子主要測試了addCamper()方法。
這里注意的就是該測試創(chuàng)建的對象和屬性都不會保存在任何store中的。
關于異步測試,這里用到了兩個context,一個root context運行在后臺線程中,另外一個main context是root context的子類,讓context分別在正確的線程中執(zhí)行其實也很簡單,主要采用下面兩種方法:
測試第二種performBlock()方法時可能會需要些技巧,因為數(shù)據(jù)可能不會立即得到,還好XCTestCase提供了一個叫expectations的新特性。下面展示了使用expectation來完成對異步方法的測試:
let expectation = self.expectationWithDescription("Done!");
someService.callMethodWithCompletionHandler() {
expectation.fulfill()
}
self.waitForExpectationsWithTimeout(2.0, handler: nil)
該特性的關鍵是要么是expectation.fulfill()被執(zhí)行,要么觸發(fā)超時產(chǎn)生一個異常expectation,這樣test才能繼續(xù)。
我們現(xiàn)在來為CamperServiceTests繼續(xù)增加一個新的方法來測試root context的保存:
func testRootContextIsSavedAfterAddingCamper() {
//1 創(chuàng)建了一個針對異步測試的方法,主要是通過觀察save方法觸發(fā)的通知,觸發(fā)通知后具體的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步?jīng)]有return true,那么就觸發(fā)error
self.waitForExpectationsWithTimeout(2.0) {
error in
XCTAssertNil(error, "Save did not occur")
}
}
這一節(jié)新建了一個CampSiteServiceTests?Class 對CampSiteService進行測試,具體code形式與上一節(jié)類似,添加了測試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.
根據(jù)需求又寫了一個testGetCampSiteWithMatchingSiteNumber()方法用來測試getCampSite(),因為campSiteService.addCampSite()方法在之前的測試方法中已經(jīng)通過測試了,所以這里可以放心去用,這就是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,當然通不過啦,我們還沒有實現(xiàn)他?,F(xiàn)在為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?
}
現(xiàn)在重新CMD+U一下,就通過了。
最后一節(jié)主要針對APP中的ReservationService類進行測試,同樣的是創(chuàng)建一個ReservationServiceTests測試類,這個test類的setUP和tearDown與第三節(jié)類似。只不過多了campSiteService與camperService的設置。在testReserveCampSitePositiveNumberOfDays()方法中對ReservationService類里的reserveCampSite()進行測試后,發(fā)現(xiàn)沒有對numberOfNights有效性進行判斷,隨后進行了修改,這也算是展示了單元測試的另一種能力。作者是這么解釋的:不管你對這些要測試的code有何了解,你盡肯能地針對這些API寫一些測試,如果OK,那么皆大歡喜,如果出問題了,那意味著要么改進code要么改進測試代碼。
更多建議: