跳转至

7.单元测试

单元测试是将一个项目分解成小的、可测试的软件片段或单元的过程。 您可以将其分解为测试更小的操作,例如按钮触摸事件、创建实体以及测试保存是否成功,而不是测试“应用程序在您点击按钮时创建新记录”的场景。

在本章中,您将学习如何在Xcode中使用 XCTest 框架来测试您的Core Data应用。 Core Data应用程序的单元测试并不像它可能的那样简单,因为大多数测试都依赖于有效的Core Data堆栈。 您可能不希望单元测试中的大量测试数据干扰您自己在模拟器或设备上进行的手动测试,因此您将了解如何将测试数据分开。

为什么要关心应用程序的单元测试? 原因有很多:

  • 您可以在非常早期的阶段就摆脱应用程序的架构和行为。 您可以测试应用程序的大部分功能,而无需担心UI
  • 您可以放心地添加功能或重构项目,而不必担心会破坏项目。 如果您有通过的现有测试,那么您可以确信,如果您稍后破坏了某些东西,测试将失败,因此您将立即知道问题。
  • 您可以让您的多个开发人员团队避免互相跌倒,因为每个开发人员都可以独立于其他人进行和测试他们的更改。
  • 可以保存测试时间。 您无需在三个不同的屏幕上点击并在字段中输入测试数据,而是可以对应用代码的任何部分运行一个小测试,而无需通过UI手动操作。

在本章中,您将很好地介绍XCTest,但是您应该已经对它有了基本的了解,才能从本章中获得最大的收获。

有关更多信息,请查看Apple的文档https://developer.apple.com/documentation/xctest,我们的iOS单元测试和UI测试教程。,或者我们的书 iOS Test-Driven Development by Tutorials,这本书深入探讨了单元测试和编写可测试代码。

入门

本章中的示例项目 CampgroundManager 是一个跟踪露营地、每个营地的设施和露营者本身的预订系统。

img

该应用程序是一个正在进行的工作。 基本概念:一个小的露营地可以使用这个应用程序来管理他们的露营地和预订,包括时间表和付款。 用户界面极其基本;它功能强大,但没有提供太多价值。 没关系-在本教程中,您永远不会构建和运行应用程序!

应用程序的业务逻辑已被分解为小块。 您将编写单元测试来帮助设计。 当您开发单元测试并充实业务逻辑时,很容易看到用户界面还需要做什么工作。

业务逻辑被分为三个不同的类,按主题排列。 有一个是露营地的,一个是露营者的,还有一个是预订的。 所有类都有后缀 Service,您的测试将集中在这些服务类上。

访问控制

默认情况下,Swift中的类具有“内部”访问级别。 这意味着您只能从它们自己的模块中访问它们。 由于应用程序和测试位于单独的目标和单独的模块中,因此您通常无法在测试中访问应用程序中的类。

有三种方法可以解决这个问题:

  1. 你可以将你的应用中的类和方法标记为public,使它们在测试中可见(或者open,允许子类化)。
  2. 您可以在文件检查器中将类添加到测试目标中,以便在测试中编译它们并从测试中访问它们。
  3. 您可以在单元测试中的任何导入前面添加Swift关键字@testable,以访问类中导入的所有内容。

CampgroundManager 示例项目中,app目标中的必要类和方法已经标记为publicopen。 这意味着你只需要从测试中导入CampgroundManager,你就可以访问你需要的任何东西。

Note

使用@testable是最简单的方法,但它在语言中的存在有些争议。 理论上,只有公共方法应该进行单元测试;任何非“公共”的东西都是不可测试的,因为没有公共接口或契约。 使用@testable绝对比盲目地将public添加到所有类和函数中更容易接受。

测试核心数据栈

由于您将测试应用程序的核心数据部分,因此第一项工作是设置核心数据堆栈以进行测试。

好的单元测试遵循首字母缩略词 FIRST

  • Fast:如果测试运行时间太长,您就不必运行它们。
  • Isolated:任何测试在单独运行时或在任何其他测试之前或之后都应该正常运行。
  • Repeatable:每次对同一代码库运行测试时,您都应该得到相同的结果。
  • Self-verifying:测试本身应报告成功或失败;你不应该检查文件或控制台日志的内容。
  • Timely:在已经编写了代码之后再编写测试是有好处的,特别是如果您要编写一个新的测试来覆盖一个新的bug。 但是,理想情况下,测试首先充当您正在开发的功能的规范。

当执行单元测试时,应用程序将启动,测试将在正在运行的应用程序的环境中运行。在实践中,如果应用程序的状态受到正在运行的测试的影响,则这可能会导致问题,反之亦然。 CampgroundManager 已设置为允许单元测试执行覆盖AppDelegate。 这可以防止应用程序干扰单元测试。 查看 main.swiftTestingAppDelegate。swift 了解更多详情。

CampgroundManager 使用Core Data将数据存储在磁盘上的数据库文件中。 这听起来并不是很孤立,因为来自一个测试的数据可能会写入数据库,并可能影响其他测试。 这听起来也不太容易执行,因为每次运行测试时数据都会在数据库文件中累积。 您可以在运行每个测试之前手动删除并重新创建数据库文件,但这并不是很糟糕。

解决方案是修改后的Core Data堆栈,它使用 in-memory SQLite store ,而不是由磁盘上的文件支持的存储。 这将是快速和提供一个干净的石板每一次。

本书大部分内容中使用的CoreDataStack默认为磁盘上的SQLite存储。 当你使用CoreDataStack进行测试时,你希望它使用内存存储。

首先,创建一个子类CoreDataStack的新类,以便您可以更改存储。

  1. 右键单击CampgroundManagerTests组下的Services,单击New File
  2. iOS ▸ Source下选择Swift File。 单击Next
  3. 将文件命名为 TestCoreDataStack。swift。 确保只选中 CampgroundManagerTests 目标。
  4. 单击Create
  5. 如果提示添加Objective-C桥接头,请选择 Don’t Create

最后,将文件的内容替换为以下内容:

import CampgroundManager
import Foundation
import CoreData

class TestCoreDataStack: CoreDataStack {
  override init() {
    super.init()

    let container = NSPersistentContainer(
      name: CoreDataStack.modelName,
      managedObjectModel: CoreDataStack.model)
    container.persistentStoreDescriptions[0].url =
      URL(fileURLWithPath: "/dev/null")

    container.loadPersistentStores { _, error in
      if let error = error as NSError? {
        fatalError(
          "Unresolved error \(error), \(error.userInfo)")
      }
    }

    self.storeContainer = container
  }
}

该类子类CoreDataStack,只覆盖单个属性的默认值:storeContainer。 由于您正在覆盖init()中的值,因此不会使用CoreDataStack中的持久化容器-甚至不会实例化。 TestCoreDataStack中的持久化容器使用的文件位置为/dev/null,即 null device 。 这是一个特殊的文件系统位置,所有数据都将被丢弃,这会导致SQLite创建一个内存存储,而不是保存在磁盘上。 您可以实例化堆栈并在测试中写入您想要的数据。 当测试结束时- * poof * -内存中的存储会自动清除。 有了堆栈,就可以创建您的第一个测试了!

Note

Core Data也有一个内存存储类型,NSInMemoryStoreType,你可以用它来测试,但是SQLite存储和内存存储之间有本质的区别。 使用内存支持的SQLite存储可以提供与应用的真实的行为最接近的匹配。

如果您的测试需要大量数据,内存中的SQLite存储可能不是最佳方法。 在这些情况下,遵循上面的方法,但使用特定于测试的URL来创建一个仅用于测试的磁盘存储。 然后,您的tearDown()方法将关闭测试存储并删除每个测试的文件。

第一次测试

当您将应用设计为小模块的集合时,单元测试效果最佳。 您创建一个类(或多个类)来封装该逻辑,而不是将所有业务逻辑都放入一个巨大的视图控制器中。

在大多数情况下,您可能会向部分完成的应用程序添加单元测试。 CampgroundManagerCamperServiceCampSiteServiceReservationService类已经创建,但功能尚未完善。 您将首先测试最简单的类CamperService

开始创建一个新的测试类:

  1. 右键单击CampgroundManagerTests组下的Services组,单击新建文件。
  2. 选择iOS ▸ Source▸ Unit Test Case Class。 单击Next
  3. 命名类 CamperServiceTests;应该已经选择了 XCTestCase 的子类。 选择Swift语言,点击Next
  4. 确保 CampgroundManagerTests 目标复选框是唯一选中的目标。 单击Create

CamperServiceTests.swift中,添加如下import语句,将appCore Data框架添加到import XCTest下面的测试用例中:

import CampgroundManager
import CoreData

接下来,将以下两个属性添加到类中:

// MARK: Properties
var camperService: CamperService!
var coreDataStack: CoreDataStack!

这些属性将保存对测试中的CamperService实例和Core Data堆栈的引用。 这些属性是隐式展开的选项,因为它们将在setUp()而不是init()中初始化。

接下来,用下面的代码替换所有的存根方法:

override func setUp() {
  super.setUp()

  coreDataStack = TestCoreDataStack()
  camperService = CamperService(
    managedObjectContext: coreDataStack.mainContext,
    coreDataStack: coreDataStack)
}

在每次测试运行之前调用setUp。 这是您创建类中所有单元测试所需的任何资源的机会。 在本例中,初始化camperServicecoreDataStack属性。

在每次测试后重置数据是明智的-您的测试是独立的和可重复的,记得吗? 使用内存中的存储并在setUp()中创建一个新的上下文可以为您完成此重置。

请注意,CoreDataStack实例实际上是TestCoreDataStack实例。 CamperService初始化方法获取它所需的上下文沿着CoreDataStack的实例,因为上下文保存方法是该类的一部分。 您也可以使用setUp()将标准测试数据插入到上下文中,以供以后使用。

接下来,直接在setUp()下面添加以下实现:

override func tearDown() {
  super.tearDown()

  camperService = nil
  coreDataStack = nil
}

tearDown()setUp()相反,在每个测试执行后调用。 在这里,该方法将简单地使所有属性为nil,在每次测试后重置CoreDataStack

此时CamperService上只有一个方法:addCamper(_:phonenumber:)。 仍在 CamperServiceTests.swift中,在tearDown()下面添加如下方法,测试addCamper

func testAddCamper() {
  let camper = camperService.addCamper(
    "Bacon Lover",
    phoneNumber: "910-543-9000")

  XCTAssertNotNil(camper, "Camper should not be nil")
  XCTAssertTrue(camper?.fullName == "Bacon Lover")
  XCTAssertTrue(camper?.phoneNumber == "910-543-9000")
}

您创建了一个具有某些属性的露营器,然后检查并确认存在具有所需属性的露营器。

这是一个简单的测试,但它确保了如果修改了addCamper中的任何逻辑,基本操作不会改变。 例如,如果您添加了一些新的数据验证逻辑来防止喜欢培根的人预订露营地,则addCamper可能会返回nil。 然后这个测试会失败,提醒您在验证中犯了一个错误,或者测试需要更新。

!!! 音符 为了在真实的的开发环境中完善这个测试用例,您可能需要为一些奇怪的场景编写单元测试,例如nil参数、空参数或重复的露营者名称。

单击Product菜单,然后选择Test(或键入Command+U),运行单元测试。 你应该在Xcode中看到一个绿色的复选标记。

img

这是你的第一个测试! 这种类型的测试对于利用数据模型和检查属性是否正确存储非常有用。

还要注意测试是如何为使用API的人提供微文档的。 这是一个如何调用addCamper的示例,并描述了预期的行为。 在这种情况下,方法应该返回一个有效的对象,而不是nil

请注意,此测试创建了对象并检查了属性,但没有将任何内容保存到存储区。 这个项目使用了一个单独的队列上下文,因此它可以在后台持久化数据。 但是,测试直接贯穿;这意味着你不能用XCTAssert检查保存结果,因为你不能确定后台操作何时完成。 保存数据是核心数据的重要组成部分-那么如何测试应用程序的这一部分呢?

异步测试

Core Data中使用单个托管对象上下文时,所有内容都在主UI线程上运行。 然而,创建背景上下文是一种常见的模式,它是主上下文的子上下文,用于在不阻塞UI的情况下进行工作。

在给定上下文的正确线程上执行工作很容易:您只需将工作包装在performBlockAndWait()performBlock()中,以确保它在与上下文关联的线程上执行。 performBlockAndWait()会等待块执行完毕后再继续,而performBlock()会立即返回并在上下文上排队执行。

测试performBlock()执行可能很棘手,因为您需要某种方式从块内部向外部世界发送有关测试状态的信号。 幸运的是,XCTestCase中有一个名为 expectations 的特性可以帮助解决这个问题。

下面的示例显示了如何使用期望来等待异步方法完成,然后再完成测试:

let expectation = expectation(withDescription: "Done!")

someService.callMethodWithCompletionHandler() {
  expectation.fulfill()
}

waitForExpectations(timeout: 2.0, handler: nil)

这里的关键是,必须有一些东西满足或触发预期,这样测试才能继续进行。 最后的wait方法接受一个时间参数(以秒为单位),因此测试不会永远等待,并且可以在期望值从未实现的情况下超时(并失败)。

在提供的示例中,在传递到测试方法中的完成处理程序中显式调用fulfill()。 使用Core Data保存操作,侦听NSManagedObjectContextDidSave通知会更容易,因为它发生在您不能显式调用fulfill()的地方。

CamperServiceTests.swift添加一个新方法,以测试添加新的camper时是否保存了根上下文:

func testRootContextIsSavedAfterAddingCamper() {
  //1
  let derivedContext = coreDataStack.newDerivedContext()
  camperService = CamperService(
    managedObjectContext: derivedContext,
    coreDataStack: coreDataStack)

  //2
  expectation(
    forNotification: .NSManagedObjectContextDidSave,
    object: coreDataStack.mainContext) { _ in
      return true
  }

  //3
  derivedContext.perform {
    let camper = self.camperService.addCamper(
      "Bacon Lover",
      phoneNumber: "910-543-9000")
    XCTAssertNotNil(camper)
  }

  //4
  waitForExpectations(timeout: 2.0) { error in
    XCTAssertNil(error, "Save did not occur")
  }
}

下面是代码的分解:

  1. 对于这个测试,您创建了一个后台上下文来完成这项工作。 使用此上下文而不是主上下文重新创建CamperService实例。
  2. 创建链接到通知的文本预期。 在这种情况下,期望被链接到来自核心数据堆栈的根上下文的NSManagedObjectContextDidSave通知。 通知的处理程序很简单:它返回true,因为您所关心的只是通知是否被触发。
  3. 您添加了camper,与前面完全相同,但这次是在派生上下文的perform块中,因为这是一个后台上下文,需要在自己的线程上运行操作。
  4. 测试等待期望值的时间最多为两秒。 如果出现错误或超时,处理程序块的error参数将包含一个值。

运行单元测试,您应该在这个新方法旁边看到一个绿色的复选标记。 重要的是要将阻止UI的操作(如Core Data保存操作)从主线程中移除,以便您的应用保持响应。

测试期望对于确保单元测试涵盖这些异步操作是非常宝贵的。

您已为应用中的现有功能添加了测试;现在是时候自己添加一些特性和测试了。 或者为了更有趣--也许先写测试?

先测试

CampgroundManager 的一个重要功能是为露营者预留场地。 在它可以接受预订之前,系统必须知道露营地的所有露营地。 CampSiteService的创建是为了帮助添加,删除和查找露营地。

打开 CampSiteService。swift,您会注意到唯一实现的方法是addCampSite。 此方法没有单元测试,因此为服务创建一个测试用例:

  1. 右键单击CampgroundManagerTests组下的Services,单击New File
  2. 选择iOS\Source\Unit Test Case Class。 单击Next
  3. 将类命名为 CampSiteServiceTests;应该已经选择了 XCTestCase 的子类。 选择Swift语言,点击Next
  4. 确保 CampgroundManagerTests 目标复选框是唯一选中的目标。 单击Create

将文件内容替换为以下内容:

import UIKit
import XCTest
import CampgroundManager
import CoreData

class CampSiteServiceTests: XCTestCase {

  // MARK: Properties
  var campSiteService: CampSiteService!
  var coreDataStack: CoreDataStack!

  override func setUp() {
    super.setUp()

    coreDataStack = TestCoreDataStack()
    campSiteService = CampSiteService(
      managedObjectContext: coreDataStack.mainContext,
      coreDataStack: coreDataStack)
  }

  override func tearDown() {
    super.tearDown()

    campSiteService = nil
    coreDataStack = nil
  }
}

这看起来与前面的测试类非常相似。 当您的测试套件扩展并且您注意到常见或重复的代码时,您可以重构您的测试以及您的应用程序代码。 这样做您会感到很安全,因为如果您搞砸了任何事情,单元测试就会失败!

添加以下新方法以测试添加露营地。 这看起来和工作方式类似于测试创建新露营者的方法:

func testAddCampSite() {
  let campSite = campSiteService.addCampSite(
    1,
    electricity: true,
    water: true)

  XCTAssertTrue(
    campSite.siteNumber == 1,
    "Site number should be 1")
  XCTAssertTrue(
    campSite.electricity!.boolValue,
    "Site should have electricity")
  XCTAssertTrue(
    campSite.water!.boolValue,
    "Site should have water")
}

要确保在此方法期间保存上下文,请将以下内容添加到测试中:

func testRootContextIsSavedAfterAddingCampsite() {
  let derivedContext = coreDataStack.newDerivedContext()

  campSiteService = CampSiteService(
    managedObjectContext: derivedContext,
    coreDataStack: coreDataStack)

  expectation(
    forNotification: .NSManagedObjectContextDidSave,
    object: coreDataStack.mainContext) { _ in
      return true
  }

  derivedContext.perform {
    let campSite = self.campSiteService.addCampSite(
      1,
      electricity: true,
      water: true)
    XCTAssertNotNil(campSite)
  }

  waitForExpectations(timeout: 2.0) { error in
    XCTAssertNil(error, "Save did not occur")
  }
}

这个方法看起来应该与您之前创建的方法非常相似。 运行单元测试;一切都会过去。 在这一点上,你应该感到有点偏执。 如果测试被破坏了,而它们总是通过了怎么办? 现在是时候进行一些测试驱动的开发,并获得来自将红色测试变为绿色的嗡嗡声了!

Note

测试驱动开发(TDD)是一种开发应用程序的方法,它首先编写测试,然后逐步实现功能,直到测试通过。 然后重构代码以用于下一个特性或改进。 介绍TDD方法超出了本章的范围,但是如果您决定遵循TDD,这里介绍的步骤将有助于您使用TDD。

CampSiteServiceTests.swift的末尾添加以下方法来测试getCampSite()

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")
}

这两个测试都使用addCampSite方法来创建一个新的CampSite。 您知道这个方法在您以前的测试中是有效的,所以没有必要再次测试它。 实际测试包括按ID检索CampSite和测试结果是否为nil

想想看,用空数据库开始每个测试是多么可靠。 如果你没有使用内存存储,很容易就会有一个营地与第二次测试的ID匹配,然后就会失败!

运行单元测试。 由于您尚未实现getCampSite,因此需要CampSite的测试失败。

img

另一个单元测试--不需要站点的单元测试--通过了。 这是一个假阳性的例子,因为该方法总是返回nil。 为每个方法添加多个场景的测试以执行尽可能多的代码路径是很重要的。

CampSiteService.swift中实现getCampSite,代码如下:

public func getCampSite(_ siteNumber: NSNumber) -> CampSite? {
  let fetchRequest: NSFetchRequest<CampSite> =
    CampSite.fetchRequest()
  fetchRequest.predicate = NSPredicate(
    format: "%K = %@",
    argumentArray: [#keyPath(CampSite.siteNumber), siteNumber])

  let results: [CampSite]?
  do {
    results = try managedObjectContext.fetch(fetchRequest)
  } catch {
    return nil
  }
  return results?.first
}

现在重新运行单元测试,您应该看到绿色的复选标记。 啊,成功的甜蜜满足!

Note

本章的最后一个项目包含在本书附带的资源中,其中包括了针对每种方法的多个场景的单元测试。 您可以浏览该代码以获得更多示例。

验证与重构

ReservationService将包含一些相当复杂的逻辑,以确定露营者是否能够预订站点。ReservationService的单元测试将要求到目前为止创建的每个服务测试其操作。

像以前一样创建一个新的测试类:

  1. 右键单击CampgroundManagerTests组下的Services,单击【新建文件】。
  2. 选择iOS ▸ Source ▸ Unit Test Case Class。 单击Next
  3. 将类命名为 ReservationServiceTests;应该已经选择了 XCTestCase 的子类。 语言选择Swift。 单击Next
  4. 确保 CampgroundManagerTests 目标复选框是唯一选中的目标。 单击Create

将文件内容替换为以下内容:

import Foundation
import CoreData
import XCTest
import CampgroundManager

class ReservationServiceTests: XCTestCase {

  // MARK: Properties
  var campSiteService: CampSiteService!
  var camperService: CamperService!
  var reservationService: ReservationService!
  var coreDataStack: CoreDataStack!

  override func setUp() {
    super.setUp()
    coreDataStack = TestCoreDataStack()
    camperService = CamperService(
      managedObjectContext: coreDataStack.mainContext,
      coreDataStack: coreDataStack)
    campSiteService = CampSiteService(
      managedObjectContext: coreDataStack.mainContext,
      coreDataStack: coreDataStack)
    reservationService = ReservationService(
      managedObjectContext: coreDataStack.mainContext,
      coreDataStack: coreDataStack)
  }

  override func tearDown() {
    super.tearDown()

    camperService = nil
    campSiteService = nil
    reservationService = nil
    coreDataStack = nil
  }
}

这是您在前面的测试用例类中使用的设置和拆除代码的稍长版本。 沿着像往常一样设置CoreData堆栈外,您还为每个测试在setUp中创建每个服务的新实例。

添加以下方法以测试创建预订:

func testReserveCampSitePositiveNumberOfDays() {
  let camper = camperService.addCamper(
    "Johnny Appleseed",
    phoneNumber: "408-555-1234")!
  let campSite = campSiteService.addCampSite(
    15,
    electricity: false,
    water: false)

  let result = reservationService.reserveCampSite(
    campSite,
    camper: camper,
    date: Date(),
    numberOfNights: 5)

  XCTAssertNotNil(
    result.reservation,
    "Reservation should not be nil")
  XCTAssertNil(
    result.error,
    "No error should be present")
  XCTAssertTrue(
    result.reservation?.status == "Reserved",
    "Status should be Reserved")
}

单元测试创建了一个露营者和露营地,这两者都是预订场地所必需的。 这里的新部分是你使用预订服务来预订露营地,将露营者和露营地联系在一起并注明日期。

单元测试验证是否创建了Reservation对象,并且NSError对象不在返回的元组中。 查看reserveCampSite调用,您可能已经意识到住宿天数至少应该大于零。 添加以下单元测试来测试该条件:

func testReserveCampSiteNegativeNumberOfDays() {
  let camper = camperService.addCamper(
    "Johnny Appleseed",
    phoneNumber: "408-555-1234")!
  let campSite = campSiteService.addCampSite(
    15,
    electricity: false,
    water: false)

  let result = reservationService!.reserveCampSite(
    campSite,
    camper: camper,
    date: Date(),
    numberOfNights: -1)

  XCTAssertNotNil(
    result.reservation,
    "Reservation should not be nil")
  XCTAssertNotNil(
    result.error,
    "An error should be present")
  XCTAssertTrue(
    result.error?.userInfo["Problem"] as? String == "Invalid number of days",
    "Error problem should be present")
  XCTAssertTrue(
    result.reservation?.status == "Invalid",
    "Status should be Invalid")
}

运行单元测试,您会注意到测试失败了。 很明显,写ReservationService的人没有想到要检查这个! 这是一件好事,你在测试中发现了这个bug,然后它才被发布到世界上--也许预订了一个负数的夜晚会级联到退款!

测试是探测系统并发现其行为漏洞的好地方。 该试验也可作为准规范;测试表明你仍然期待一个有效的,非nil的结果,但是设置了错误条件。

首先打开 ReservationService.swift,找到reserveCampSite(_:camper:date:numberOfNights:)并替换:

reservation.status = "Reserved"

为以下的内容:

if numberOfNights <= 0 {
  reservation.status = "Invalid"
  registrationError = NSError(
    domain: "CampingManager",
    code: 5,
    userInfo: ["Problem": "Invalid number of days"])
} else {
  reservation.status = "Reserved"
}

最后,将registrationError从常量改为变量,将let替换为var

现在重新运行测试并检查负数天数测试是否通过。 您可以看到当您想要添加其他功能或验证规则时,重构过程是如何继续的。

无论您是否知道正在测试的代码的细节,还是将其视为一个黑盒,您都可以针对API编写这些类型的测试,以查看它是否按预期运行。 如果是这样,那就太好了! 这意味着测试将确保您的代码按预期运行。 如果不是,您需要更改测试以匹配代码,或者更改代码以匹配测试。

关键点

  • 单元测试应该遵循 FIRST 原则:快速、隔离、可重复、自验证和及时。
  • 为单元测试创建一个特定的持久存储,并在每次测试时重置其内容。 使用内存中的SQLite存储是最简单的方法。
  • Core Data可以异步使用,并且很容易使用XCTestExpectation类进行测试。

接下来去哪?

你可能已经听过很多次了,单元测试你的工作是维护一个稳定软件产品的关键。 虽然Core Data可以帮助您从项目中消除大量容易出错的持久性代码,但如果使用不当,它可能会成为逻辑错误的来源。

编写可以使用Core Data的单元测试将有助于在代码到达用户之前稳定代码。 XCTestExpectation是一个简单但功能强大的工具,可以帮助您以异步方式测试核心数据。 明智地使用它!

作为一个挑战,CampSiteService有许多尚未实现的方法,用TODO注释标记。 使用TDD方法,编写单元测试,然后实现使测试通过的方法。 如果您遇到了问题,请查看本章参考资料中包含的challenge项目,以获得示例解决方案。