💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
> 原文出处: 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要么改进测试代码。