I am creating a framework that allows me to use Core Data. In the framework's test target, I have configured a data model named MockModel.xcdatamodeld
. It contains a single entity named MockManaged
that has a single Date
property.
So that I can test my logic, I am creating an in-memory store. When I want to validate my saving logic, I create an instance of the in-memory store and use it. However, I keep getting the following output in the console:
2018-08-14 20:35:45.340157-0400 xctest[7529:822360] [error] warning: Multiple NSEntityDescriptions claim the NSManagedObject subclass 'LocalPersistenceTests.MockManaged' so +entity is unable to disambiguate. CoreData: warning: Multiple NSEntityDescriptions claim the NSManagedObject subclass 'LocalPersistenceTests.MockManaged' so +entity is unable to disambiguate. 2018-08-14 20:35:45.340558-0400 xctest[7529:822360] [error] warning: 'MockManaged' (0x7f986861cae0) from NSManagedObjectModel (0x7f9868604090) claims 'LocalPersistenceTests.MockManaged'. CoreData: warning: 'MockManaged' (0x7f986861cae0) from NSManagedObjectModel (0x7f9868604090) claims 'LocalPersistenceTests.MockManaged'. 2018-08-14 20:35:45.340667-0400 xctest[7529:822360] [error] warning: 'MockManaged' (0x7f986acc4d10) from NSManagedObjectModel (0x7f9868418ee0) claims 'LocalPersistenceTests.MockManaged'. CoreData: warning: 'MockManaged' (0x7f986acc4d10) from NSManagedObjectModel (0x7f9868418ee0) claims 'LocalPersistenceTests.MockManaged'. 2018-08-14 20:35:45.342938-0400 xctest[7529:822360] [error] error: +[LocalPersistenceTests.MockManaged entity] Failed to find a unique match for an NSEntityDescription to a managed object subclass CoreData: error: +[LocalPersistenceTests.MockManaged entity] Failed to find a unique match for an NSEntityDescription to a managed object subclass
Below is the object I use to create my in-memory stores:
class MockNSManagedObjectContextCreator { // MARK: - NSManagedObjectContext Creation static func inMemoryContext() -> NSManagedObjectContext { guard let model = NSManagedObjectModel.mergedModel(from: [Bundle(for: self)]) else { fatalError("Could not create model") } let coordinator = NSPersistentStoreCoordinator(managedObjectModel: model) do { try coordinator.addPersistentStore(ofType: NSInMemoryStoreType, configurationName: nil, at: nil, options: nil) } catch { fatalError("Could not create in-memory store") } let context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) context.persistentStoreCoordinator = coordinator return context } }
Below is what makes up my MockManaged
entity:
class MockManaged: NSManagedObject, Managed { // MARK: - Properties @NSManaged var date: Date }
Below is what makes up my XCTestCase
:
class Tests_NSManagedObjectContext: XCTestCase { // MARK: - Object Insertion func test_NSManagedObjectContext_InsertsManagedObject_WhenObjectConformsToManagedProtocol() { let context = MockNSManagedObjectContextCreator.inMemoryContext() let changeExpectation = expectation(forNotification: .NSManagedObjectContextObjectsDidChange, object: context, handler: nil) let object: MockManaged = context.insertObject() object.date = Date() wait(for: [changeExpectation], timeout: 2) } // MARK: - Saving func test_NSManagedObjectContext_Saves_WhenChangesHaveBeenMade() { let context = MockNSManagedObjectContextCreator.inMemoryContext() let saveExpectation = expectation(forNotification: .NSManagedObjectContextDidSave, object: context, handler: nil) let object: MockManaged = context.insertObject() object.date = Date() do { try context.saveIfHasChanges() } catch { XCTFail("Expected successful save") } wait(for: [saveExpectation], timeout: 2) } func test_NSManagedObjectContext_DoesNotSave_WhenNoChangesHaveBeenMade() { let context = MockNSManagedObjectContextCreator.inMemoryContext() let saveExpectation = expectation(forNotification: .NSManagedObjectContextDidSave, object: context, handler: nil) saveExpectation.isInverted = true do { try context.saveIfHasChanges() } catch { XCTFail("Unexpected error: \(error)") } wait(for: [saveExpectation], timeout: 2) } }
What am I doing that is causing the errors in my tests?
This should not happen anymore with NSPersistent[CloudKit]Container(name: String)
, since it seems to cache the model automatically now (Swift 5.1, Xcode11, iOS13/MacOS10.15).
NSPersistentContainer/NSPersistentCloudKitContainer
does have two constructors:
The first is just a convenience initializer calling the second with a model loaded from disk. The trouble is that loading the same NSManagedObjectModel
twice from disk inside the same app/test invocation
results in the errors above, since every loading of the model results in external registration calls, which print errors once called a second time on the same app/test invocation
. And init(name: String)
was not smart enough to cache the model.
So if you want to load a container multiple time you have to load the NSManagedObjectModel
once and store it in an attribute you then use on every init(name:managedObjectModel:)
call.
import Foundation import SwiftUI import CoreData import CloudKit class PersistentContainer { private static var _model: NSManagedObjectModel? private static func model(name: String) throws -> NSManagedObjectModel { if _model == nil { _model = try loadModel(name: name, bundle: Bundle.main) } return _model! } private static func loadModel(name: String, bundle: Bundle) throws -> NSManagedObjectModel { guard let modelURL = bundle.url(forResource: name, withExtension: "momd") else { throw CoreDataError.modelURLNotFound(forResourceName: name) } guard let model = NSManagedObjectModel(contentsOf: modelURL) else { throw CoreDataError.modelLoadingFailed(forURL: modelURL) } return model } enum CoreDataError: Error { case modelURLNotFound(forResourceName: String) case modelLoadingFailed(forURL: URL) } public static func container() throws -> NSPersistentCloudKitContainer { let name = "ItmeStore" return NSPersistentCloudKitContainer(name: name, managedObjectModel: try model(name: name)) } }
Loading Core Data is a little bit of magic, where loading a model from disk and using it means it registers for certain types. A second loading tries to register for the type again, which obviously tells you that something registered for the type already.
You can load Core Data only once and cleanup that instance after each test. Cleanup means deleting every object entity and then saving. There is some function which gives you all entities which you can then fetch and delete. Batch delete is not available InMemory though so object-by-managed object it is there.
The (probably simpler) alternative is to load the model once, store it somewhere and reuse that model on every NSPersistentContainer
call, it has a constructor to use a given model instead of loading it again from disk.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With