I am currently unit testing a layer that interacts with Core Data. It saves, deletes, and updates an Item
object. However, my test that attempts to save a couple of Item
s and then perform a batch deletion keeps failing.
This is Item
:
extension Item {
// MARK: - Properties
@NSManaged public var date: NSDate
@NSManaged public var isTaxable: Bool
@NSManaged public var name: String
@NSManaged public var price: NSDecimalNumber
@NSManaged public var quantity: Double
// MARK: - Fetch Requests
@nonobjc public class func fetchRequest() -> NSFetchRequest<Item> { return NSFetchRequest<Item>(entityName: "Item") }
// MARK: - Validation
// Manual validation for `Decimal` values is needed. A radar is still open, which is located at https://openradar.appspot.com/13677527.
public override func validateValue(_ value: AutoreleasingUnsafeMutablePointer<AnyObject?>, forKey key: String) throws {
if key == "price", let decimal = value.pointee as? Decimal { if decimal < Decimal(0.01) { throw NSError(domain: NSCocoaErrorDomain, code: 1620, userInfo: ["Item": self]) } }
if key == "quantity", let double = value.pointee as? Double { if double == 0 { throw NSError(domain: NSCocoaErrorDomain, code: 1620, userInfo: ["Item": self]) } }
}
}
This is the object that interacts with Core Data, CoreDataStack
:
internal class CoreDataStack {
// MARK: - Properties
private let modelName: String
internal lazy var storeContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: self.modelName)
container.loadPersistentStores { (storeDescription, error) in if let error = error as NSError? { fatalError("Unresolved error \(error), \(error.userInfo)") } }
return container
}()
internal lazy var managedContext: NSManagedObjectContext = { return self.storeContainer.viewContext }()
// MARK: - Initialization
internal init(modelName: String = "Cart") { self.modelName = modelName }
// MARK: - Saving
internal func saveContext() throws {
guard managedContext.hasChanges else { return }
do { try managedContext.save() } catch let error as NSError { throw error }
}
}
This is the object that manages persistence with Core Data:
internal final class ItemPersistenceService {
// MARK: - Properties
private let coreDataStack: CoreDataStack
// MARK: - Initialization
internal init(coreDataStack: CoreDataStack) {
self.coreDataStack = coreDataStack
print("init(coreDataStack:) - ItemPersistenceService")
}
// MARK: - Saving
@discardableResult internal func saveItem(withInformation information: ItemInformation) throws -> Item {
let item = Item(context: coreDataStack.managedContext)
item.name = information.name
item.quantity = information.quantity
item.price = information.price as NSDecimalNumber
item.date = information.date as NSDate
item.isTaxable = information.isTaxable
do {
try coreDataStack.saveContext()
} catch let error as NSError {
throw error
}
return item
}
// MARK: - Deleting
internal func delete(item: Item) throws {
coreDataStack.managedContext.delete(item)
do {
try coreDataStack.saveContext()
} catch let error as NSError {
throw error
}
}
internal func deleteAllItems() throws {
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: Item.description())
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
do {
try coreDataStack.managedContext.persistentStoreCoordinator?.execute(deleteRequest, with: coreDataStack.managedContext)
} catch let error as NSError {
throw error
}
}
// MARK: - Fetching
internal func itemsCount() throws -> Int {
let fetchRequest = NSFetchRequest<NSNumber>(entityName: Item.description())
fetchRequest.resultType = .countResultType
do {
let result = try coreDataStack.managedContext.fetch(fetchRequest)
guard let count = result.first?.intValue else { fatalError("Invalid result") }
return count
} catch {
throw error
}
}
}
This is the CoreDataStack
subclass that I use for testing, which contains an in-memory store:
internal final class TestCoreDataStack: CoreDataStack {
// MARK: - Initialization
internal override init(modelName: String = "Cart") {
super.init(modelName: modelName)
let persistentStoreDescription = NSPersistentStoreDescription()
persistentStoreDescription.type = NSInMemoryStoreType
let container = NSPersistentContainer(name: modelName)
container.persistentStoreDescriptions = [persistentStoreDescription]
container.loadPersistentStores { (storeDescription, error) in
if let error = error as NSError? { fatalError("Unresolved error \(error), \(error.userInfo)") }
self.storeContainer = container
}
}
}
Finally, this is the test that keeps failing:
internal func test_ItemPersistenceService_Delete_All_Managed_Object_Context_Saved() {
do {
try service.saveItem(withInformation: information)
try service.saveItem(withInformation: information)
} catch { XCTFail("Expected `Item`") }
expectation(forNotification: .NSManagedObjectContextDidSave, object: coreDataStack.managedContext) { (notification) in return true }
do { try service.deleteAllItems() } catch { XCTFail("Expected deletion") }
waitForExpectations(timeout: 2.0) { error in XCTAssertNil(error, "Expected save to occur") }
}
Questions
Is NSInMemoryStoreType incompatible with NSBatchDeleteRequest?
If not, then what am I doing incorrectly that is causing my test to fail repeatedly?
I was hoping to use the same method for deleting a large number of objects efficiently too, but this page states that the
NSBatchDeleteRequest
is only compatible with SQLite persistent store types, In-memory store types are not supported.https://developer.apple.com/library/content/featuredarticles/CoreData_Batch_Guide/BatchDeletes/BatchDeletes.html
The different persistent store types are listed here:
https://developer.apple.com/documentation/coredata/nspersistentstorecoordinator/persistent_store_types
You can always create a persistent store of type SQLite and store it at
/dev/null
. Here's the code to do it on a swiftXCTest
class: