codecamp

Core Data by tutorials 笔记(六)

原文出处: http://chengway.in/post/ji-zhu/core-data-by-tutorials-bi-ji-liu

今天我们来关注一下CoreData的单元测试,其实在写程序之前,先写测试,将问题一点点分解,也是TDD所倡导的做法,这也是我今年所期望达成的一个目标,新开项目按TDD的流程来做,以后也会整理些这方面的东西。如果你对CoreData的其他方面感兴趣请查看我之前的笔记或直接购买《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:

• 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创建一个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的逻辑拆分出来,我们创建一个类来封装这些逻辑。作者这里创建的第一个测试类为CamperServiceTestsXCTestCase的子类,用来测试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会在每次测试前被调用,这里可以创建一些测试需要用到东西,而且因为使用的是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要么改进测试代码。

Core Data by tutorials 笔记(五)
Core Data by tutorials 笔记(七)
温馨提示
下载编程狮App,免费阅读超1000+编程语言教程
取消
确定
目录

关闭

MIP.setData({ 'pageTheme' : getCookie('pageTheme') || {'day':true, 'night':false}, 'pageFontSize' : getCookie('pageFontSize') || 20 }); MIP.watch('pageTheme', function(newValue){ setCookie('pageTheme', JSON.stringify(newValue)) }); MIP.watch('pageFontSize', function(newValue){ setCookie('pageFontSize', newValue) }); function setCookie(name, value){ var days = 1; var exp = new Date(); exp.setTime(exp.getTime() + days*24*60*60*1000); document.cookie = name + '=' + value + ';expires=' + exp.toUTCString(); } function getCookie(name){ var reg = new RegExp('(^| )' + name + '=([^;]*)(;|$)'); return document.cookie.match(reg) ? JSON.parse(document.cookie.match(reg)[2]) : null; }