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

版本記錄

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

前言

數(shù)據(jù)的持久化存儲(chǔ)是移動(dòng)端不可避免的一個(gè)問題扶歪,很多時(shí)候的業(yè)務(wù)邏輯都需要我們進(jìn)行本地化存儲(chǔ)解決和完成,我們可以采用很多持久化存儲(chǔ)方案扬舒,比如說plist文件(屬性列表)、preference(偏好設(shè)置)、NSKeyedArchiver(歸檔)宝恶、SQLite 3乘综、CoreData憎账,這里基本上我們都用過。這幾種方案各有優(yōu)缺點(diǎn)卡辰,其中胞皱,CoreData是蘋果極力推薦我們使用的一種方式邪意,我已經(jīng)將它分離出去一個(gè)專題進(jìn)行說明講解。這個(gè)專題主要就是針對(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ǔ)示例(二)
13. 數(shù)據(jù)持久化方案解析(十三) —— 基于Unit Testing的Core Data測(cè)試(一)

源碼

1. Swift

首先看下工程組織結(jié)構(gòu)

接著看一下sb中的內(nèi)容

下面就是源碼了

1. PandemicReport+CoreDataProperties.swift
import Foundation
import CoreData

extension PandemicReport {
  @nonobjc
  public class func fetchRequest() -> NSFetchRequest<PandemicReport> {
    return NSFetchRequest<PandemicReport>(entityName: "PandemicReport")
  }

  @NSManaged public var id: UUID?
  @NSManaged public var location: String?
  @NSManaged public var numberTested: Int32
  @NSManaged public var numberPositive: Int32
  @NSManaged public var numberNegative: Int32
  @NSManaged public var dateReported: Date?
}
2. PandemicReport+CoreDataClass.swift
import Foundation
import CoreData

@objc(PandemicReport)
public class PandemicReport: NSManagedObject {
}
3. CoreDataStack.swift
import Foundation
import CoreData

open class CoreDataStack {
  public static let modelName = "PandemicReport"

  public static let model: NSManagedObjectModel = {
    // swiftlint:disable force_unwrapping
    let modelURL = Bundle.main.url(forResource: modelName, withExtension: "momd")!
    return NSManagedObjectModel(contentsOf: modelURL)!
  }()
  // swiftlint:enable force_unwrapping

  public init() {
  }

  public lazy var mainContext: NSManagedObjectContext = {
    return storeContainer.viewContext
  }()

  public lazy var storeContainer: NSPersistentContainer = {
    let container = NSPersistentContainer(name: CoreDataStack.modelName, managedObjectModel: CoreDataStack.model)
    container.loadPersistentStores { _, error in
      if let error = error as NSError? {
        fatalError("Unresolved error \(error), \(error.userInfo)")
      }
    }
    return container
  }()

  public func newDerivedContext() -> NSManagedObjectContext {
    let context = storeContainer.newBackgroundContext()
    return context
  }

  public func saveContext() {
    saveContext(mainContext)
  }

  public func saveContext(_ context: NSManagedObjectContext) {
    if context != mainContext {
      saveDerivedContext(context)
      return
    }

    context.perform {
      do {
        try context.save()
      } catch let error as NSError {
        fatalError("Unresolved error \(error), \(error.userInfo)")
      }
    }
  }

  public func saveDerivedContext(_ context: NSManagedObjectContext) {
    context.perform {
      do {
        try context.save()
      } catch let error as NSError {
        fatalError("Unresolved error \(error), \(error.userInfo)")
      }

      self.saveContext(self.mainContext)
    }
  }
}
4. ReportService.swift
import Foundation
import CoreData

public final class ReportService {
  // MARK: - Properties
  let managedObjectContext: NSManagedObjectContext
  let coreDataStack: CoreDataStack

  // MARK: - Initializers
  public init(managedObjectContext: NSManagedObjectContext, coreDataStack: CoreDataStack) {
    self.managedObjectContext = managedObjectContext
    self.coreDataStack = coreDataStack
  }
}

// MARK: - Public
extension ReportService {
  @discardableResult
  public func add(_ location: String, numberTested: Int32, numberPositive: Int32, numberNegative: Int32) -> PandemicReport {
    let report = PandemicReport(context: managedObjectContext)
    report.id = UUID()
    report.dateReported = Date()
    report.numberTested = numberTested
    report.numberNegative = numberNegative
    report.numberPositive = numberPositive
    report.location = location

    coreDataStack.saveContext(managedObjectContext)
    return report
  }

  public func getReports() -> [PandemicReport]? {
    let reportFetch: NSFetchRequest<PandemicReport> = PandemicReport.fetchRequest()
    do {
      let results = try managedObjectContext.fetch(reportFetch)
      return results
    } catch let error as NSError {
      print("Fetch error: \(error) description: \(error.userInfo)")
    }
    return nil
  }

  @discardableResult
  public func update(_ report: PandemicReport) -> PandemicReport {
    coreDataStack.saveContext(managedObjectContext)
    return report
  }

  public func delete(_ report: PandemicReport) {
    managedObjectContext.delete(report)
    coreDataStack.saveContext(managedObjectContext)
  }
}
5. ViewController.swift
import UIKit
import CoreData
class ViewController: UIViewController {
  // MARK: - Properties
  @IBOutlet private weak var tableView: UITableView!
  private lazy var coreDataStack = CoreDataStack()
  private lazy var reportService = ReportService(
    managedObjectContext: coreDataStack.mainContext,
    coreDataStack: coreDataStack)
  private var reports: [PandemicReport]?
  private let segueIdentifier = "showDetail"
  private let cellIdentifier = "Cell"

  override func viewDidLoad() {
    super.viewDidLoad()
    tableView.delegate = self
    tableView.dataSource = self
  }

  override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    reports = reportService.getReports()
    tableView.reloadData()
  }

  // MARK: - Navigation
  override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    guard
      segue.identifier == segueIdentifier,
      let navigationController = segue.destination as? UINavigationController,
      let controller = navigationController.topViewController as? ReportDetailsTableViewController
      else {
        return
    }

    navigationController.modalPresentationStyle = .fullScreen
    controller.reportService = reportService
    if let indexPath = tableView.indexPathForSelectedRow, let existingReport = reports?[indexPath.row] {
      controller.report = existingReport
    }
  }

  // MARK: - Actions
  @IBAction func add(_ sender: Any) {
    performSegue(withIdentifier: segueIdentifier, sender: nil)
  }
}

// MARK: - UITableViewDataSource
extension ViewController: UITableViewDelegate {
  func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    performSegue(withIdentifier: segueIdentifier, sender: nil)
  }
}

// MARK: - UITableViewDataSource
extension ViewController: UITableViewDataSource {
  func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return reports?.count ?? 0
  }

  func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = self.tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath)
    guard let report = reports?[indexPath.row] else {
      return cell
    }
    cell.textLabel?.text = report.location
    return cell
  }

  func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
    return true
  }

  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)
    reportService.delete(report)
    tableView.deleteRows(at: [indexPath], with: .automatic)
  }
}
6. ReportDetailsTableViewController.swift
import UIKit

class ReportDetailsTableViewController: UITableViewController {
  // MARK: - Properties
  var report: PandemicReport?
  var reportService: ReportService?
  @IBOutlet weak var locationTextField: UITextField!
  @IBOutlet weak var numberTestedTextField: UITextField!
  @IBOutlet weak var numberPositiveTextField: UITextField!
  @IBOutlet weak var numberNegativeTextField: UITextField!
  @IBOutlet weak var dateReportedLabel: UILabel!

  override func viewDidLoad() {
    super.viewDidLoad()

    // disables cell highlighting
    tableView.allowsSelection = false

    let formatter = DateFormatter()
    formatter.dateStyle = .short

    // Display values of selected report
    if let report = report {
      locationTextField.text = report.location
      numberTestedTextField.text = "\(report.numberTested)"
      numberPositiveTextField.text = "\(report.numberPositive)"
      numberNegativeTextField.text = "\(report.numberNegative)"
      dateReportedLabel.text = formatter.string(from: report.dateReported ?? Date())
    } else {
      dateReportedLabel.text = formatter.string(from: Date())
    }
  }

  // MARK: - Actions
  @IBAction func cancel(_ sender: Any) {
    dismiss(animated: true, completion: nil)
  }

  @IBAction func save(_ sender: Any) {
    let location = locationTextField.text ?? ""
    let numberTested = Int32(numberTestedTextField.text ?? "") ?? 0
    let numberPositive = Int32(numberPositiveTextField.text ?? "") ?? 0
    let numberNegative = Int32(numberNegativeTextField.text ?? "") ?? 0

    if let report = report {
      report.location = location
      report.numberTested = numberTested
      report.numberPositive = numberPositive
      report.numberNegative = numberNegative
      reportService?.update(report)
      dismiss(animated: true, completion: nil)
    } else {
      reportService?.add(
        location,
        numberTested: numberTested,
        numberPositive: numberPositive,
        numberNegative: numberNegative)
      dismiss(animated: true, completion: nil)
    }
  }
}
7. TestCoreDataStack.swift
import Foundation
import CoreData
import PandemicReport

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

    let persistentStoreDescription = NSPersistentStoreDescription()
    persistentStoreDescription.type = NSInMemoryStoreType

    let container = NSPersistentContainer(
      name: CoreDataStack.modelName,
      managedObjectModel: CoreDataStack.model)
    container.persistentStoreDescriptions = [persistentStoreDescription]

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

    storeContainer = container
  }
}
8. ReportServiceTests.swift
import XCTest
@testable import PandemicReport
import CoreData

class ReportServiceTests: XCTestCase {
  // MARK: - Properties
  // swiftlint:disable implicitly_unwrapped_optional
  var reportService: ReportService!
  var coreDataStack: CoreDataStack!
  // swiftlint:enable implicitly_unwrapped_optional

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

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

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

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

  func testRootContextIsSavedAfterAddingReport() {
    let derivedContext = coreDataStack.newDerivedContext()
    reportService = ReportService(managedObjectContext: derivedContext, coreDataStack: coreDataStack)

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

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

      XCTAssertNotNil(report)
    }

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

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

    let getReports = reportService.getReports()

    XCTAssertNotNil(getReports)
    XCTAssertTrue(getReports?.count == 1)
    XCTAssertTrue(newReport.id == getReports?.first?.id)
  }

  func testUpdateReport() {
    let newReport = reportService.add("Snow Planet", numberTested: 0, numberPositive: 0, numberNegative: 0)
    newReport.numberTested = 30
    newReport.numberPositive = 10
    newReport.numberNegative = 20
    newReport.location = "Hoth"
    let updatedReport = reportService.update(newReport)

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

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

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

    reportService.delete(newReport)

    fetchReports = reportService.getReports()

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

后記

本篇主要講述了基于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)離奇詭異,居然都是意外死亡同衣,警方通過查閱死者的電腦和手機(jī)竟块,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,970評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來耐齐,“玉大人浪秘,你說我怎么就攤上這事〔嚎觯” “怎么了耸携?”我有些...
    開封第一講書人閱讀 168,983評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)辕翰。 經(jīng)常有香客問我夺衍,道長(zhǎng),這世上最難降的妖魔是什么喜命? 我笑而不...
    開封第一講書人閱讀 59,938評(píng)論 1 299
  • 正文 為了忘掉前任沟沙,我火速辦了婚禮,結(jié)果婚禮上壁榕,老公的妹妹穿的比我還像新娘矛紫。我一直安慰自己,他們只是感情好牌里,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,955評(píng)論 6 398
  • 文/花漫 我一把揭開白布颊咬。 她就那樣靜靜地躺著,像睡著了一般二庵。 火紅的嫁衣襯著肌膚如雪贪染。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,549評(píng)論 1 312
  • 那天催享,我揣著相機(jī)與錄音杭隙,去河邊找鬼。 笑死因妙,一個(gè)胖子當(dāng)著我的面吹牛痰憎,可吹牛的內(nèi)容都是我干的票髓。 我是一名探鬼主播,決...
    沈念sama閱讀 41,063評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼铣耘,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼洽沟!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起蜗细,我...
    開封第一講書人閱讀 39,991評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤裆操,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后炉媒,有當(dāng)?shù)厝嗽跇淞掷锇l(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
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望奔害。 院中可真熱鬧楷兽,春花似錦、人聲如沸华临。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,572評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽雅潭。三九已至揭厚,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間扶供,已是汗流浹背筛圆。 一陣腳步聲響...
    開封第一講書人閱讀 33,671評(píng)論 1 274
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(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