数据持久化方案解析(十四) —— 基于Unit Testing的Core Data测试(二)

版本记录

版本号 时间
V1.0 2020.08.19 星期三

前言

数据的持久化存储是移动端不可避免的一个问题,很多时候的业务逻辑都需要我们进行本地化存储解决和完成,我们可以采用很多持久化存储方案,比如说plist文件(属性列表)、preference(偏好设置)、NSKeyedArchiver(归档)、SQLite 3CoreData,这里基本上我们都用过。这几种方案各有优缺点,其中,CoreData是苹果极力推荐我们使用的一种方式,我已经将它分离出去一个专题进行说明讲解。这个专题主要就是针对另外几种数据持久化存储方案而设立。
1. 数据持久化方案解析(一) —— 一个简单的基于SQLite持久化方案示例(一)
2. 数据持久化方案解析(二) —— 一个简单的基于SQLite持久化方案示例(二)
3. 数据持久化方案解析(三) —— 基于NSCoding的持久化存储(一)
4. 数据持久化方案解析(四) —— 基于NSCoding的持久化存储(二)
5. 数据持久化方案解析(五) —— 基于Realm的持久化存储(一)
6. 数据持久化方案解析(六) —— 基于Realm的持久化存储(二)
7. 数据持久化方案解析(七) —— 基于Realm的持久化存储(三)
8. 数据持久化方案解析(八) —— UIDocument的数据存储(一)
9. 数据持久化方案解析(九) —— UIDocument的数据存储(二)
10. 数据持久化方案解析(十) —— UIDocument的数据存储(三)
11. 数据持久化方案解析(十一) —— 基于Core Data 和 SwiftUI的数据存储示例(一)
12. 数据持久化方案解析(十二) —— 基于Core Data 和 SwiftUI的数据存储示例(二)
13. 数据持久化方案解析(十三) —— 基于Unit Testing的Core Data测试(一)

源码

1. Swift

首先看下工程组织结构

接着看一下sb中的内容

下面就是源码了

1. PandemicReport+CoreDataProperties.swift
import Foundation
import CoreData

extension PandemicReport {
  @nonobjc
  public class func fetchRequest() -> NSFetchRequest {
    return NSFetchRequest(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.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测试,感兴趣的给个赞或者关注~~~

你可能感兴趣的:(数据持久化方案解析(十四) —— 基于Unit Testing的Core Data测试(二))