數(shù)據(jù)持久化方案解析(十三) —— 基于Unit Testing的Core Data測(cè)試(一)

版本記錄

版本號(hào) 時(shí)間
V1.0 2020.08.19 星期三

前言

數(shù)據(jù)的持久化存儲(chǔ)是移動(dòng)端不可避免的一個(gè)問(wèn)題梗顺,很多時(shí)候的業(yè)務(wù)邏輯都需要我們進(jìn)行本地化存儲(chǔ)解決和完成,我們可以采用很多持久化存儲(chǔ)方案分俯,比如說(shuō)plist文件(屬性列表)、preference(偏好設(shè)置)、NSKeyedArchiver(歸檔)辣吃、SQLite 3榆纽、CoreData仰猖,這里基本上我們都用過(guò)捏肢。這幾種方案各有優(yōu)缺點(diǎn),其中饥侵,CoreData是蘋(píng)果極力推薦我們使用的一種方式鸵赫,我已經(jīng)將它分離出去一個(gè)專(zhuān)題進(jìn)行說(shuō)明講解。這個(gè)專(zhuān)題主要就是針對(duì)另外幾種數(shù)據(jù)持久化存儲(chǔ)方案而設(shè)立躏升。
1. 數(shù)據(jù)持久化方案解析(一) —— 一個(gè)簡(jiǎn)單的基于SQLite持久化方案示例(一)
2. 數(shù)據(jù)持久化方案解析(二) —— 一個(gè)簡(jiǎn)單的基于SQLite持久化方案示例(二)
3. 數(shù)據(jù)持久化方案解析(三) —— 基于NSCoding的持久化存儲(chǔ)(一)
4. 數(shù)據(jù)持久化方案解析(四) —— 基于NSCoding的持久化存儲(chǔ)(二)
5. 數(shù)據(jù)持久化方案解析(五) —— 基于Realm的持久化存儲(chǔ)(一)
6. 數(shù)據(jù)持久化方案解析(六) —— 基于Realm的持久化存儲(chǔ)(二)
7. 數(shù)據(jù)持久化方案解析(七) —— 基于Realm的持久化存儲(chǔ)(三)
8. 數(shù)據(jù)持久化方案解析(八) —— UIDocument的數(shù)據(jù)存儲(chǔ)(一)
9. 數(shù)據(jù)持久化方案解析(九) —— UIDocument的數(shù)據(jù)存儲(chǔ)(二)
10. 數(shù)據(jù)持久化方案解析(十) —— UIDocument的數(shù)據(jù)存儲(chǔ)(三)
11. 數(shù)據(jù)持久化方案解析(十一) —— 基于Core Data 和 SwiftUI的數(shù)據(jù)存儲(chǔ)示例(一)
12. 數(shù)據(jù)持久化方案解析(十二) —— 基于Core Data 和 SwiftUI的數(shù)據(jù)存儲(chǔ)示例(二)

開(kāi)始

首先看下主要內(nèi)容:

測(cè)試代碼是應(yīng)用程序開(kāi)發(fā)的關(guān)鍵部分辩棒,Core Data也不例外。 本教程將教您如何測(cè)試Core Data膨疏。內(nèi)容來(lái)自翻譯一睁。

下面就是寫(xiě)作環(huán)境:

Swift 5, iOS 13, Xcode 11

測(cè)試代碼是應(yīng)用開(kāi)發(fā)過(guò)程中至關(guān)重要的一部分。 盡管測(cè)試最初需要一段時(shí)間才能習(xí)慣佃却,但它還是有很多好處者吁,例如:

  • 允許您進(jìn)行更改,而不必?fù)?dān)心應(yīng)用程序的某些部分會(huì)損壞饲帅。
  • 加速調(diào)試過(guò)程复凳。
  • 迫使您考慮如何以更有條理的方式組織代碼。

并且在本教程中灶泵,您將學(xué)習(xí)如何將測(cè)試的好處應(yīng)用于Core Data模型育八。

您將使用PandemicReport,這是一個(gè)簡(jiǎn)單但出色的大流行報(bào)告跟蹤器赦邻。 您將專(zhuān)注于為項(xiàng)目的Core Data模型編寫(xiě)單元測(cè)試髓棋,并學(xué)習(xí):

  • 什么是單元測(cè)試及其重要性。
  • 如何編寫(xiě)適合測(cè)試的Core Data Stack惶洲。
  • 如何對(duì)Core Data模型進(jìn)行單元測(cè)試仲锄。
  • 關(guān)于TDD方法論。

注意:本教程假定您了解Core Data的基礎(chǔ)知識(shí)湃鹊。 如果您不熟悉Core Data儒喊,請(qǐng)首先查看 Getting Started with Core Data Tutorial

在入門(mén)項(xiàng)目中币呵,您會(huì)找到PandemicTracker怀愧,這是一個(gè)顯示感染報(bào)告列表的應(yīng)用程序霞赫。

您可以添加新報(bào)告并編輯現(xiàn)有報(bào)告蚌讼。 該應(yīng)用程序使用Core Data持久保存會(huì)話(huà)之間的報(bào)告。 構(gòu)建并運(yùn)行以簽出該應(yīng)用程序早龟。

該應(yīng)用程序顯示保存在Core Data中的大流行報(bào)告列表妻柒。 目前扛拨,您沒(méi)有任何報(bào)告。 通過(guò)單擊導(dǎo)航欄中的添加按鈕來(lái)添加一個(gè)举塔。

然后绑警,通過(guò)在text field中輸入值來(lái)添加報(bào)告條目求泰。

接下來(lái),點(diǎn)擊Save以將您的報(bào)告保存到Core Data并關(guān)閉此屏幕计盒。

現(xiàn)在渴频,該列表包含您的條目。

在Xcode中北启,查看要處理的主要文件:

  • CoreDataStack.swift:對(duì)象包裝器卜朗,用于管理應(yīng)用的Core Data model層。
  • ReportService.swift:管理應(yīng)用的業(yè)務(wù)邏輯咕村。
  • ViewController.swift:顯示保存在Core Data中的報(bào)告列表场钉。 點(diǎn)擊+將顯示新報(bào)告的輸入表單。
  • ReportDetailsTableViewController.swift:顯示所選報(bào)告的詳細(xì)信息懈涛,并允許您編輯現(xiàn)有值逛万。 當(dāng)您在ViewController中點(diǎn)擊+時(shí),這也充當(dāng)輸入形式肩钠。

您將探索為什么為Core Data編寫(xiě)單元測(cè)試會(huì)比后面幾節(jié)中講的要棘手泣港。


What is Unit Testing?

單元測(cè)試是將項(xiàng)目分解為較小的可測(cè)試代碼段的任務(wù)暂殖。 例如价匠,您可以將iPhone上的Messages邏輯分解為較小的功能單元,如下所示:

  • 將一個(gè)或多個(gè)收件人分配給該郵件呛每。
  • 在文本區(qū)域中寫(xiě)入文本踩窖。
  • 添加表情符號(hào)。
  • 添加圖像晨横。
  • 附加GIF洋腮。
  • 附加Animoji

盡管這似乎是很多額外的工作手形,但是測(cè)試有很多好處:

  • 單元測(cè)試可驗(yàn)證您的代碼是否按預(yù)期工作啥供。
  • 編寫(xiě)測(cè)試可以在bug投入生產(chǎn)之前就將它們捕獲。
  • 測(cè)試還充當(dāng)其他開(kāi)發(fā)人員的文檔库糠。
  • 與手動(dòng)測(cè)試相比伙狐,單元測(cè)試可以節(jié)省時(shí)間。
  • 在開(kāi)發(fā)過(guò)程中失敗的測(cè)試使您知道某些問(wèn)題瞬欧。

在iOS中贷屎,單元測(cè)試在與您要測(cè)試的應(yīng)用程序相同的環(huán)境中運(yùn)行。 因此艘虎,如果正在運(yùn)行的測(cè)試修改了應(yīng)用的狀態(tài)唉侄,則可能會(huì)導(dǎo)致問(wèn)題。

注意:如果您要開(kāi)始在iOS中進(jìn)行測(cè)試野建,或者想復(fù)習(xí)一下属划,請(qǐng)查看iOS Unit Testing and UI Testing恬叹。


CoreData Stack for Testing

該項(xiàng)目的Core Data stack當(dāng)前使用SQLite數(shù)據(jù)庫(kù)作為其存儲(chǔ)。運(yùn)行測(cè)試時(shí)榴嗅,您不希望測(cè)試或虛擬數(shù)據(jù)干擾應(yīng)用程序的存儲(chǔ)妄呕。

要編寫(xiě)好的單元測(cè)試,請(qǐng)遵循首字母縮寫(xiě)詞FIRST

  • Fast:?jiǎn)卧獪y(cè)試運(yùn)行迅速嗽测。
  • Isolated:它們應(yīng)獨(dú)立于其他測(cè)試運(yùn)行绪励。
  • Repeatable:每次執(zhí)行測(cè)試都應(yīng)產(chǎn)生相同的結(jié)果。
  • Self-verifying:測(cè)試應(yīng)該通過(guò)或失敗唠粥。您無(wú)需檢查控制臺(tái)或日志文件即可確定測(cè)試是否成功疏魏。
  • Timely:首先編寫(xiě)測(cè)試,以便它們可以充當(dāng)您添加的功能的藍(lán)圖晤愧。

Core Data將數(shù)據(jù)寫(xiě)入并保存到模擬器或設(shè)備上的數(shù)據(jù)庫(kù)文件中大莫。由于一項(xiàng)測(cè)試可能會(huì)覆蓋另一項(xiàng)測(cè)試的內(nèi)容,因此您不能將其視為Isolated官份。

由于數(shù)據(jù)保存到磁盤(pán)只厘,因此數(shù)據(jù)庫(kù)中的數(shù)據(jù)會(huì)隨著時(shí)間增長(zhǎng),并且每次測(cè)試運(yùn)行時(shí)環(huán)境的狀態(tài)可能會(huì)有所不同舅巷。結(jié)果羔味,這些測(cè)試是不可Repeatable

測(cè)試完成后钠右,刪除并重新創(chuàng)建數(shù)據(jù)庫(kù)內(nèi)容并不Fast赋元。

您可能會(huì)想,“好吧飒房,我猜我無(wú)法測(cè)試Core Data搁凸,因?yàn)樗鼰o(wú)法測(cè)試”。再想一想狠毯。

解決方案是創(chuàng)建一個(gè)使用內(nèi)存存儲(chǔ)in-memory store而不是當(dāng)前SQLite存儲(chǔ)的Core Data stack子類(lèi)护糖。由于內(nèi)存存儲(chǔ)區(qū)不會(huì)持久存儲(chǔ)在磁盤(pán)上,因此當(dāng)測(cè)試完成執(zhí)行時(shí)嚼松,in-memory store將釋放其數(shù)據(jù)嫡良。

您將在下一部分中創(chuàng)建此子類(lèi)。

1. Adding the TestCoreDataStack

首先惜颇,在PandemicReportTests組下創(chuàng)建CoreDataStack的子類(lèi)皆刺,并將其命名為TestCoreDataStack.swift

接著凌摄,添加下面到文件中:

import CoreData
import PandemicReport

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

    // 1
    let persistentStoreDescription = NSPersistentStoreDescription()
    persistentStoreDescription.type = NSInMemoryStoreType

    // 2
    let container = NSPersistentContainer(
      name: CoreDataStack.modelName,
      managedObjectModel: CoreDataStack.model)

    // 3
    container.persistentStoreDescriptions = [persistentStoreDescription]

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

    // 4
    storeContainer = container
  }
}

在這里羡蛾,代碼:

  • 1) 創(chuàng)建一個(gè)內(nèi)存持久存儲(chǔ)(persistent store)
  • 2) 創(chuàng)建一個(gè)NSPersistentContainer實(shí)例锨亏,并傳入存儲(chǔ)在CoreDataStack中的modelNameNSManageObjectModel痴怨。
  • 3) 將in-memory persistent store分配給容器忙干。
  • 4) 覆蓋CoreDataStack中的storeContainer

真好浪藻! 有了這個(gè)類(lèi)捐迫,您就有了為Core Data Model創(chuàng)建測(cè)試的基線(xiàn)

2. Different Stores

在上面,您使用了一個(gè)內(nèi)存存儲(chǔ)(in-memory store)爱葵,但是您可能想知道還有哪些其他選擇施戴。 Core Data中有四個(gè)永久存儲(chǔ)(persistent store)可用:

  • NSSQLiteStoreType:用于Core Data的最常見(jiàn)存儲(chǔ)由SQLite數(shù)據(jù)庫(kù)支持。 Xcode的Core Data Template默認(rèn)情況下使用此代碼萌丈,同時(shí)也是項(xiàng)目中使用的store赞哗。
  • NSXMLStoreType:由XML文件支持。
  • NSBinaryStoreType:由二進(jìn)制數(shù)據(jù)文件支持辆雾。
  • NSInMemoryStoreType:此存儲(chǔ)類(lèi)型會(huì)將數(shù)據(jù)保存到內(nèi)存肪笋,因此不會(huì)持久保存。 這對(duì)于單元測(cè)試很有用度迂,因?yàn)槿绻麘?yīng)用終止藤乙,數(shù)據(jù)就會(huì)消失。

注意:如果您想了解有關(guān)Core Data中不同存貯的更多信息惭墓,請(qǐng)查看Apple Documentation on Persistent Store Types坛梁。

完成此步驟后,就該編寫(xiě)第一個(gè)測(cè)試了诅妹。


Writing Your First Test

PandemicReport中罚勾,ReportService是用于處理CRUD邏輯的小類(lèi)毅人。 CRUDCreate-Read-Update-Delete(持久存儲(chǔ)最常見(jiàn)的功能)的首字母縮寫(xiě)吭狡。 您將編寫(xiě)單元測(cè)試以驗(yàn)證該功能是否正常運(yùn)行。

要編寫(xiě)測(cè)試丈莺,您將使用Xcode中的XCTest框架和XCTestCase子類(lèi)划煮。

首先在PandemicReportTests下創(chuàng)建一個(gè)新的Unit Test Case Class,并將其命名為ReportServiceTests.swift缔俄。

然后弛秋,在ReportServiceTests.swift中,在import XCTest下添加以下代碼:

@testable import PandemicReport
import CoreData

此代碼將應(yīng)用程序和CoreData框架導(dǎo)入到您的測(cè)試用例中俐载。

接下來(lái)蟹略,將以下兩個(gè)屬性添加到ReportServiceTests的頂部:

var reportService: ReportService!
var coreDataStack: CoreDataStack!

這些屬性包含對(duì)要測(cè)試的ReportServiceCoreDataStack的引用。

返回ReportServiceTests.swift遏佣,刪除以下內(nèi)容:

  • 1) setUpWithError()
  • 2) tearDownWithError()
  • 3) testExample()
  • 4) testPerformanceExample()

您將看到為什么接下來(lái)不需要它們的原因挖炬。

1. The Set Up and Tear Down

您的單元測(cè)試應(yīng)該是孤立且可重復(fù)的(isolated and repeatable)XCTestCase有兩種方法setUp()tearDown()状婶,用于在每次運(yùn)行之前設(shè)置測(cè)試用例并在之后清除所有測(cè)試數(shù)據(jù)意敛。 由于每個(gè)測(cè)試都從一個(gè)干凈的表盤(pán)開(kāi)始馅巷,因此這些方法有助于使您的測(cè)試孤立且可重復(fù)。

在聲明的屬性下添加以下代碼:

override func setUp() {
  super.setUp()
  coreDataStack = TestCoreDataStack()
  reportService = ReportService(
    managedObjectContext: coreDataStack.mainContext,
    coreDataStack: coreDataStack)
}

在這里草姻,您將初始化您先前實(shí)現(xiàn)的TestCoreDataStack以及ReportService钓猬。 如前所述,TestCoreDataStack使用內(nèi)存中的存儲(chǔ)(in-memory store)撩独,并在每次執(zhí)行setUp()時(shí)進(jìn)行初始化敞曹。 因此,所有創(chuàng)建的PandemicReport都不會(huì)在每次測(cè)試之間持久存在综膀。

另一方面异雁,tearDown()會(huì)在每次測(cè)試運(yùn)行后重置數(shù)據(jù)。

返回ReportServiceTests僧须,添加以下內(nèi)容:

override func tearDown() {
  super.tearDown()
  reportService = nil
  coreDataStack = nil
}

這段代碼將屬性設(shè)置為nil纲刀,為下一次測(cè)試做準(zhǔn)備。

完成set uptear down后担平,您現(xiàn)在可以專(zhuān)注于測(cè)試報(bào)告的CRUD示绊。

2. Adding a Report

現(xiàn)在,您將通過(guò)編寫(xiě)一個(gè)簡(jiǎn)單的測(cè)試來(lái)檢驗(yàn)該應(yīng)用程序的現(xiàn)有功能暂论,以驗(yàn)證ReportServiceadd(_:numberTested:numberPositive:numberNegative :)功能面褐。

仍在ReportServiceTests中,創(chuàng)建一個(gè)新方法:

func testAddReport() {
  // 1
  let report = reportService.add(
    "Death Star", 
    numberTested: 1000,
    numberPositive: 999, 
    numberNegative: 1)

  // 2
  XCTAssertNotNil(report, "Report should not be nil")
  XCTAssertTrue(report.location == "Death Star")
  XCTAssertTrue(report.numberTested == 1000)
  XCTAssertTrue(report.numberPositive == 999)
  XCTAssertTrue(report.numberNegative == 1)
  XCTAssertNotNil(report.id, "id should not be nil")
  XCTAssertNotNil(report.dateReported, "dateReported should not be nil")
}

此測(cè)試驗(yàn)證add(_:numberTested:numberPositive:numberNegative :)創(chuàng)建具有指定值的PandemicReport取胎。

您添加的代碼:

  • 1) 創(chuàng)建一個(gè)PandemicReport展哭。
  • 2) 斷言輸入值與創(chuàng)建的PandemicReport相匹配。

要運(yùn)行此測(cè)試闻蛀,請(qǐng)單擊Product > Test或按Command + U作為快捷方式匪傍。 或者,您可以打開(kāi)Test導(dǎo)航器觉痛,然后選擇PandemicReportsTest并單擊play役衡。

該項(xiàng)目將構(gòu)建并運(yùn)行測(cè)試。 您會(huì)看到一個(gè)綠色的選中標(biāo)記薪棒。

恭喜你手蝎! 您已經(jīng)編寫(xiě)了第一個(gè)測(cè)試。

接下來(lái)俐芯,您將學(xué)習(xí)如何測(cè)試異步代碼棵介。


Testing Asynchronous Code

保存數(shù)據(jù)是Core Data最重要的任務(wù)。 雖然您的測(cè)試很棒吧史,但它不會(huì)測(cè)試數(shù)據(jù)是否保存到持久性存儲(chǔ)中邮辽。 它可以直接運(yùn)行,因?yàn)樵搼?yīng)用程序使用一個(gè)單獨(dú)的隊(duì)列在后臺(tái)保留數(shù)據(jù)。

將數(shù)據(jù)保存在主線(xiàn)程上可能會(huì)阻塞UI逆巍,使其無(wú)響應(yīng)及塘。 但是,測(cè)試異步代碼要復(fù)雜一些锐极。 具體來(lái)說(shuō)笙僚,由于您不知道后臺(tái)任務(wù)何時(shí)完成,因此XCTAssert無(wú)法測(cè)試您的數(shù)據(jù)是否保存灵再。

您可以通過(guò)將add(_:numberTested:numberPositive:numberNegative :)調(diào)用包裝在perform(_ :)中肋层,在與當(dāng)前上下文關(guān)聯(lián)的線(xiàn)程上執(zhí)行工作來(lái)解決此問(wèn)題。 然后翎迁,您需要將perform(_ :)expectation配對(duì)栋猖,以在保存完成時(shí)通知測(cè)試。

這就是它的樣子汪榔。

1. Testing Save

仍在ReportServiceTests內(nèi)蒲拉,添加:

func testRootContextIsSavedAfterAddingReport() {
  // 1
  let derivedContext = coreDataStack.newDerivedContext()
  reportService = ReportService(
    managedObjectContext: derivedContext,
    coreDataStack: coreDataStack)
    
  // 2
  expectation(
    forNotification: .NSManagedObjectContextDidSave,
    object: coreDataStack.mainContext) { _ in
      return true
  }

  // 3
  derivedContext.perform {
    let report = self.reportService.add(
      "Death Star 2", 
      numberTested: 600,
      numberPositive: 599, 
      numberNegative: 1)

    XCTAssertNotNil(report)
  }

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

這是這樣做的:

  • 1) 創(chuàng)建背景上下文和使用該上下文的ReportService的新實(shí)例。
  • 2) 創(chuàng)建一個(gè)expectation痴腌,該期望在Core Data stack發(fā)送NSManagedObjectContextDidSave通知事件時(shí)將信號(hào)發(fā)送到測(cè)試用例雌团。
  • 3) 它將在perform(_ :)塊內(nèi)添加一個(gè)新報(bào)告。
  • 4) 測(cè)試等待報(bào)告保存的信號(hào)士聪。 如果等待時(shí)間超過(guò)兩秒锦援,則測(cè)試將失敗。

期望是測(cè)試異步代碼時(shí)的強(qiáng)大工具剥悟,因?yàn)槠谕梢允鼓鷷和4a并等待異步任務(wù)完成灵寺。

注意:要了解有關(guān)以預(yù)期方式測(cè)試異步操作的更多信息,請(qǐng)查看有關(guān)該主題的Apple文檔Apple’s Documentation区岗。

現(xiàn)在略板,運(yùn)行測(cè)試,并在其旁邊看到一個(gè)綠色的選中標(biāo)記躏尉。

編寫(xiě)測(cè)試可以幫助您發(fā)現(xiàn)bugs并提供有關(guān)函數(shù)行為的文檔蚯根。但是后众,如果您編寫(xiě)了失敗的測(cè)試并且始終通過(guò)了該怎么辦胀糜?現(xiàn)在該做一些TDD了!


Test Driven Development (TDD)

Test Driven Development, or TDD 是一個(gè)開(kāi)發(fā)過(guò)程蒂誉,您可以在生產(chǎn)代碼之前編寫(xiě)測(cè)試教藻。通過(guò)首先編寫(xiě)測(cè)試,您可以確保代碼可測(cè)試并開(kāi)發(fā)為滿(mǎn)足所有要求右锨。

首先括堤,編寫(xiě)最少的代碼以使測(cè)試通過(guò)。然后,您逐步對(duì)功能進(jìn)行微小更改并重復(fù)悄窃。

TDD的好處之一是您的測(cè)試可以充當(dāng)應(yīng)用程序工作方式的文檔讥电。隨著功能集隨著時(shí)間的推移而擴(kuò)展,您的測(cè)試也將隨之?dāng)U展轧抗,并且據(jù)此擴(kuò)展您的文檔恩敌。

因此,單元測(cè)試是了解應(yīng)用程序特定部分工作方式的好方法横媚。如果您需要復(fù)習(xí)或從另一個(gè)開(kāi)發(fā)人員那里接管代碼庫(kù)纠炮,它們將非常有用。

TDD的其他好處包括:

  • Code Coverage:因?yàn)槟谏a(chǎn)代碼之前編寫(xiě)測(cè)試灯蝴,所以未經(jīng)測(cè)試的代碼的可能性很小恢口。
  • Confidence in refactoring:由于代碼覆蓋范圍廣,并且該項(xiàng)目被分解為較小的可測(cè)試單元穷躁,因此可以更輕松地對(duì)代碼庫(kù)進(jìn)行大型重構(gòu)耕肩。
  • Focused:您編寫(xiě)的代碼最少,可以通過(guò)測(cè)試问潭,因此您的代碼庫(kù)整潔看疗,冗余程度也較低。

The Red, Green, Refactor Cycle

好的單元測(cè)試是失敗睦授,可重復(fù)两芳,運(yùn)行迅速且易于維護(hù)的。 通過(guò)使用TDD去枷,可以確保您的測(cè)試值得怖辆。

開(kāi)發(fā)人員通常將TDD流程描述為red-green-refactor周期:

  • 1) Red:寫(xiě)一個(gè)首先失敗的測(cè)試。
  • 2) Green:編寫(xiě)盡可能少的代碼以使其通過(guò)删顶。
  • 3) Refactor:修改和優(yōu)化代碼竖螃。
  • 4) Repeat:重復(fù)這些步驟,直到您認(rèn)為代碼可以正常工作為止逗余。

注意:如果您想了解有關(guān)iOS中TDD的更多信息特咆,請(qǐng)查看我們的教程 Test Driven Development Tutorial for iOS: Getting Started

有了關(guān)于TDD的一些理論录粱,現(xiàn)在是時(shí)候?qū)DD付諸實(shí)踐了腻格。


Fetching Reports

為了熟悉TDD,您將編寫(xiě)一個(gè)驗(yàn)證getReports()的測(cè)試啥繁。 首先菜职,測(cè)試將失敗,但是您將努力確保測(cè)試不會(huì)失敗旗闽。

ReportServiceTests.swift中酬核,添加:

func testGetReports() {
  //1
  let newReport = reportService.add(
    "Endor", 
    numberTested: 30,
    numberPositive: 20, 
    numberNegative: 10)
    
  //2
  let getReports = reportService.getReports()

  //3
  XCTAssertNil(getReports)

  //4
  XCTAssertEqual(getReports?.isEmpty, true)

  //5
  XCTAssertTrue(newReport.id != getReports?.first?.id)
}

這段代碼:

  • 1) 添加一個(gè)新報(bào)告并將其分配給newReport蜜另。
  • 2) 獲取當(dāng)前存儲(chǔ)在Core Data中的所有報(bào)告,并將它們分配給getReports嫡意。
  • 3) 驗(yàn)證getReports的結(jié)果是否為nil举瑰。 這是一個(gè)失敗的測(cè)試。 getReports()應(yīng)該返回添加的報(bào)告蔬螟。
  • 4) 斷言結(jié)果數(shù)組為空嘶居。
  • 5) 斷言newReport.id不等于getReports中的第一個(gè)對(duì)象。

運(yùn)行單元測(cè)試促煮。 您會(huì)看到測(cè)試失敗邮屁。

查看失敗測(cè)試中的assert表達(dá)式的結(jié)果:

該測(cè)試失敗,因?yàn)閳?bào)表服務(wù)返回您添加的報(bào)表菠齿。 斷言與getReports返回的條件相反佑吝。 如果此測(cè)試確實(shí)通過(guò)了,則說(shuō)明getReports()無(wú)法正常工作绳匀,或者單元測(cè)試中存在bug芋忿。

要使測(cè)試從紅色變?yōu)榫G色,請(qǐng)使用以下命令替換斷言:

XCTAssertNotNil(getReports)

XCTAssertTrue(getReports?.count == 1)

XCTAssertTrue(newReport.id == getReports?.first?.id)

這段代碼:

  • 1) 檢查getReports是否為nil疾棵。
  • 2) 驗(yàn)證報(bào)告數(shù)為1戈钢。
  • 3) 斷言創(chuàng)建的報(bào)表的id,并且報(bào)表數(shù)組中的第一個(gè)結(jié)果匹配是尔。

接下來(lái)殉了,重新運(yùn)行單元測(cè)試。 您會(huì)看到一個(gè)綠色的對(duì)號(hào)拟枚。

成功薪铜! 您已將失敗的測(cè)試變成綠色,確認(rèn)代碼和測(cè)試有效且不是錯(cuò)誤恩溅。

接著隔箍,下一個(gè)。


Updating a Report

現(xiàn)在脚乡,編寫(xiě)一個(gè)測(cè)試來(lái)驗(yàn)證update(_ :)的行為是否符合預(yù)期蜒滩。 添加以下測(cè)試方法:

func testUpdateReport() {
  //1
  let newReport = reportService.add(
    "Snow Planet", 
    numberTested: 0,
    numberPositive: 0, 
    numberNegative: 0)

  //2
  newReport.numberTested = 30
  newReport.numberPositive = 10
  newReport.numberNegative = 20
  newReport.location = "Hoth"

  //3
  let updatedReport = reportService.update(newReport)

  //4
  XCTAssertFalse(newReport.id == updatedReport.id)

  //5
  XCTAssertFalse(updatedReport.numberTested == 30)
  XCTAssertFalse(updatedReport.numberPositive == 10)
  XCTAssertFalse(updatedReport.numberNegative == 20)
  XCTAssertFalse(updatedReport.location == "Hoth")
}

這段代碼:

  • 1) 創(chuàng)建一個(gè)新報(bào)告并將其分配給newReport
  • 2) 將newReport的當(dāng)前屬性更改為新值奶稠。
  • 3) 調(diào)用update:保存所做的更改俯艰,并將更新后的值分配給updatedReport
  • 4) 斷言newReport.idupdatedReport.id不匹配窒典。
  • 5) 確保updatedReport屬性不等于分配給newReport的值蟆炊。

運(yùn)行單元測(cè)試。 您會(huì)看到測(cè)試失敗瀑志。

查看單元測(cè)試,注意五個(gè)斷言都失敗了。

他們失敗了劈猪,因?yàn)?code>newReport屬性等于updatedReport的屬性昧甘。 換句話(huà)說(shuō),您希望測(cè)試在這里失敗战得。

testUpdateReport()中的斷言更新為:

XCTAssertTrue(newReport.id == updatedReport.id)
XCTAssertTrue(updatedReport.numberTested == 30)
XCTAssertTrue(updatedReport.numberPositive == 10)
XCTAssertTrue(updatedReport.numberNegative == 20)
XCTAssertTrue(updatedReport.location == "Hoth")

更新的代碼測(cè)試newReport.id是否等于updatedReport.id充边,以及updatedReport屬性是否等于分配給newReport的屬性。

重新運(yùn)行測(cè)試常侦,看看它們現(xiàn)在通過(guò)了浇冰。

到目前為止,您所做的工作比根本沒(méi)有測(cè)試要好得多聋亡,但是您仍然沒(méi)有真正遵循TDD的做法肘习。 為此,您必須在編寫(xiě)應(yīng)用程序中的實(shí)際功能之前編寫(xiě)測(cè)試坡倔。


Extending Functionality

到目前為止漂佩,您已經(jīng)添加了測(cè)試以支持該應(yīng)用程序的現(xiàn)有功能。 現(xiàn)在罪塔,您將通過(guò)擴(kuò)展服務(wù)的功能來(lái)將TDD技能提升到一個(gè)新的水平投蝉。

為此,您將添加刪除記錄的功能征堪。 由于這次您將遵循真正的TDD練習(xí)瘩缆,因此必須在生產(chǎn)代碼之前編寫(xiě)測(cè)試。

構(gòu)建并運(yùn)行并添加報(bào)告佃蚜。 添加報(bào)告后咳榜,可以通過(guò)在單元格上輕掃并點(diǎn)擊Delete來(lái)將其刪除。

但是您僅從ViewController.swift中的reports實(shí)例中刪除了該報(bào)表爽锥。 該報(bào)告仍存在于Core Data Store中涌韩,并且在您再次構(gòu)建并運(yùn)行時(shí)將返回。

要進(jìn)行檢查氯夷,請(qǐng)重新構(gòu)建并運(yùn)行臣樱。

該報(bào)告仍然存在,因?yàn)?code>ReportService.swift沒(méi)有刪除功能腮考。 接下來(lái)雇毫,您將添加此內(nèi)容。

但是首先踩蔚,您必須編寫(xiě)一個(gè)測(cè)試棚放,該測(cè)試具有刪除功能所期望的所有功能。

ReportServiceTests.swift中馅闽,添加:

func testDeleteReport() {
    //1
    let newReport = reportService.add(
      "Starkiller Base", 
      numberTested: 100,
      numberPositive: 80, 
      numberNegative: 20)

    //2
    var fetchReports = reportService.getReports()
    XCTAssertTrue(fetchReports?.count == 1)
    XCTAssertTrue(newReport.id == fetchReports?.first?.id)

    //3
    reportService.delete(newReport)

    //4    
    fetchReports = reportService.getReports()

    //5
    XCTAssertTrue(fetchReports?.isEmpty ?? false)
  }

這段代碼:

  • 1) 添加一個(gè)新的報(bào)告飘蚯。
  • 2) 從store中獲取包含報(bào)告的所有報(bào)告馍迄。
  • 3) 在reportService上調(diào)用delete刪除報(bào)告。 由于此方法尚不存在局骤,因此將失敗攀圈。
  • 4) 再次從store獲取所有報(bào)告。
  • 5) 聲明報(bào)表數(shù)組為空峦甩。

添加此代碼后赘来,您會(huì)看到編譯錯(cuò)誤。 目前凯傲,ReportService.swift沒(méi)有實(shí)現(xiàn)delete(_ :)犬辰。 接下來(lái),您將添加它冰单。

1. Deleting a Report

現(xiàn)在幌缝,打開(kāi)ReportService.swift并在類(lèi)末尾添加以下代碼:

public func delete(_ report: PandemicReport) {
  // TODO: Delete record from CoreData
}

在這里,您添加了一個(gè)空的聲明球凰。 記住狮腿,TDD規(guī)則之一是編寫(xiě)足夠的代碼以使測(cè)試通過(guò)。

您解決了編譯錯(cuò)誤呕诉。 重新運(yùn)行測(cè)試缘厢,您將看到一個(gè)失敗的測(cè)試。

現(xiàn)在測(cè)試失敗了甩挫,因?yàn)槲磸?code>store中刪除該記錄贴硫。 當(dāng)前,ReportService.swift中的delete方法具有空主體伊者,因此實(shí)際上沒(méi)有任何內(nèi)容被刪除英遭。 接下來(lái),您將對(duì)其進(jìn)行修復(fù)亦渗。

返回ReportService.swift挖诸,添加以下實(shí)現(xiàn)到delete(_:)

//1
managedObjectContext.delete(report)
//2
coreDataStack.saveContext(managedObjectContext)

代碼:

  • 1) 從持久性存儲(chǔ)(persistent store)中刪除報(bào)告。
  • 2) 將更改保存在當(dāng)前上下文中法精。

添加該代碼后多律,重新運(yùn)行單元測(cè)試。 您會(huì)看到綠色的選中標(biāo)記搂蜓。

做得好狼荞! 您已使用TDD周期向應(yīng)用程序添加了刪除功能。

完成后帮碰,轉(zhuǎn)到ViewController.swift并將tableView(_:commit:forRowAt :)替換為以下內(nèi)容:

func tableView(
  _ tableView: UITableView,
  commit editingStyle: UITableViewCell.EditingStyle,
  forRowAt indexPath: IndexPath
) {
  guard 
    let report = reports?[indexPath.row], 
    editingStyle == .delete 
    else {
      return
  }
  reports?.remove(at: indexPath.row)
  
  //1
  reportService.delete(report)
  
  tableView.deleteRows(at: [indexPath], with: .automatic)
}

在這里相味,您將delete(_ :)從報(bào)表數(shù)組中刪除后,將其調(diào)用殉挽。 現(xiàn)在丰涉,如果您滑動(dòng)并刪除拓巧,該報(bào)告將從SQLite支持的數(shù)據(jù)庫(kù)中刪除。

構(gòu)建并運(yùn)行以進(jìn)行檢查昔搂。

很好地將測(cè)試引入具有Core Data的項(xiàng)目玲销。

在本教程中输拇,您學(xué)習(xí)了如何:

  • 使用內(nèi)存存儲(chǔ)(in-memory store)編寫(xiě)可測(cè)試的Core Data stack摘符。
  • 為現(xiàn)有功能和新功能編寫(xiě)測(cè)試。
  • 測(cè)試異步代碼策吠。
  • 應(yīng)用Test Driven Development (TDD)方法逛裤。

如果遇到挑戰(zhàn),請(qǐng)嘗試編寫(xiě)一個(gè)或多個(gè)測(cè)試猴抹,以防止為numberTested带族,numberPositive和numberNegative添加非負(fù)數(shù)。

如果您喜歡本教程蟀给,請(qǐng)查看iOS Test-Driven Development by Tutorials蝙砌。您將通過(guò)先編寫(xiě)測(cè)試或?qū)y(cè)試添加到已編寫(xiě)的應(yīng)用中來(lái)深入研究如何編寫(xiě)可維護(hù)和可持續(xù)的應(yīng)用。您還應(yīng)該查看 Test Driven Development Tutorial for iOS: Getting Started跋理。

如果您想了解更多有關(guān)Core Data的信息择克,請(qǐng)查看 Getting Started with Core Data Tutorial

后記

本篇主要講述了基于Unit TestingCore Data測(cè)試前普,感興趣的給個(gè)贊或者關(guān)注~~~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末肚邢,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子拭卿,更是在濱河造成了極大的恐慌骡湖,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,378評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件峻厚,死亡現(xiàn)場(chǎng)離奇詭異响蕴,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)惠桃,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,970評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén)浦夷,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人刽射,你說(shuō)我怎么就攤上這事军拟。” “怎么了誓禁?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,983評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵懈息,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我摹恰,道長(zhǎng)辫继,這世上最難降的妖魔是什么怒见? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,938評(píng)論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮姑宽,結(jié)果婚禮上遣耍,老公的妹妹穿的比我還像新娘。我一直安慰自己炮车,他們只是感情好舵变,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,955評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著瘦穆,像睡著了一般纪隙。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上扛或,一...
    開(kāi)封第一講書(shū)人閱讀 52,549評(píng)論 1 312
  • 那天绵咱,我揣著相機(jī)與錄音,去河邊找鬼熙兔。 笑死悲伶,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的住涉。 我是一名探鬼主播麸锉,決...
    沈念sama閱讀 41,063評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼秆吵!你這毒婦竟也來(lái)了淮椰?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,991評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤纳寂,失蹤者是張志新(化名)和其女友劉穎主穗,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體毙芜,經(jīng)...
    沈念sama閱讀 46,522評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡忽媒,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,604評(píng)論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了腋粥。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片晦雨。...
    茶點(diǎn)故事閱讀 40,742評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖隘冲,靈堂內(nèi)的尸體忽然破棺而出闹瞧,到底是詐尸還是另有隱情,我是刑警寧澤展辞,帶...
    沈念sama閱讀 36,413評(píng)論 5 351
  • 正文 年R本政府宣布奥邮,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏洽腺。R本人自食惡果不足惜脚粟,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,094評(píng)論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望蘸朋。 院中可真熱鬧核无,春花似錦、人聲如沸藕坯。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,572評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)堕担。三九已至已慢,卻和暖如春曲聂,著一層夾襖步出監(jiān)牢的瞬間霹购,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,671評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工朋腋, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留齐疙,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,159評(píng)論 3 378
  • 正文 我出身青樓旭咽,卻偏偏與公主長(zhǎng)得像贞奋,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子穷绵,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,747評(píng)論 2 361