How to expose CoreData to swift package unit tests?

1.2k Views Asked by At

I'm trying to test CoreData in my swift package as SPM now supports bundled resources including .xcdatamodel, my tests can't seem to locate my NSManagedObjects though. What are the steps to unit test core data from the tests?

I'm getting this error when i try to create a NSManagedObject from a test:

+entityForName: could not locate an entity named 'StriveUser' in this model. (NSInternalInconsistencyException)

I've triple checked the naming and it's all correct.

I'm creating the object like this from my tests:

let object = NSEntityDescription.insertNewObject(forEntityName: "StriveUser", into: self.inMemoryStore.context)

And here's my code for locating the .xcdatamodel:

    fileprivate var managedObjectModel: NSManagedObjectModel = {
    guard let managedObjectModel = NSManagedObjectModel.mergedModel(from: [Bundle.main]) else {
        preconditionFailure("Error getting ManagedObjectModel")
    }

    return managedObjectModel
}()

final class InMemoryStore {
    let context: NSManagedObjectContext

    init() {
        let description = NSPersistentStoreDescription()
        description.type = NSInMemoryStoreType
        description.shouldAddStoreAsynchronously = false

        let container = NSPersistentContainer(name: Constants.modelName, managedObjectModel: managedObjectModel)
        container.persistentStoreDescriptions = [description]
        container.loadPersistentStores {_, error in
            if let error = error {
                fatalError("Failed to load store: \(error.localizedDescription)")
            }
        }

        self.context = container.viewContext
    }
}
3

There are 3 best solutions below

1
On BEST ANSWER

If you declare a Swift tools version of 5.3 or later in your package manifest, you can bundle resources with your source code as Swift packages. For example, Swift packages can contain asset catalogs, storyboards, and so on.

When resources are defined, a new static Bundle reference is created for the package. This can be accessed using Bundle.module.

So for your ManagedObjectModel you will need update the bundle reference. A good way of using this is to have an accessor in your package that will return the model.

For additional information, you can check out Apple's developer documentation Bundling Resources with a Swift Package.

0
On

I built a new bare-bones Swift Package to demonstrate the error that seems to be a cause of these issues, as of Xcode Version 13.3.1 (13E500a)

No NSEntityDescriptions in any model claim the NSManagedObject subclass 'TestModel.EntityMO' so +entity is confused. Have you loaded your NSManagedObjectModel yet ?

Breaking just after the model was loaded, I could see the entity existed:

(lldb) po model
(<NSManagedObjectModel: 0x6000015f8d70>) isEditable 1, entities {
Entity = "(<NSEntityDescription: 0x6000001c4b00>) name Entity, managedObjectClassName TestModel_TestModel.EntityMO, 
<snip>

The managedObjectClassName seems to be the problem. It's the result of using Current Product Module in the class definition within the model, which appears to be concatenating the package top-level name and the containing folder in Sources. If I replace it with a hard-coded module of TestModel, then the error goes away and the test passes. Not ideal, but it worked in my case.

Xcode support for Core Data in swift packages seems to be a work-in-progress, as the editor still does not load correctly for .xcdatamodeld files. Just creating the test model had to be done in another project and moved to the package since I couldn't add an entity to an empty model file.

For reference, I'll also include my model initialization, which is very basic, but I believe reasonable relative to Apple guidelines. At the very least, it demonstrates that issues can exist beyond Bundle.module usage.

public struct TestModel {
    internal static let modelURL = Bundle.module.url(forResource: "Model", withExtension: "momd")!

    public static func persistentContainer() -> NSPersistentContainer {
        let model = NSManagedObjectModel(contentsOf: modelURL)!
    
        let description = NSPersistentStoreDescription()
        description.type = NSInMemoryStoreType
        let container = NSPersistentContainer(name: "Test", managedObjectModel: model)
        container.persistentStoreDescriptions = [description]
    
        container.loadPersistentStores { storeDescription, error in
            guard error == nil else {
                fatalError("Could not load persistent stores. \(error!)")
            }
        }

        return container
    }
}
0
On

I ran into a similar issue, where my app would crash with "YourManagedObject not found" errors any time I tried to do anything with my core data models.

This started happening as soon as I moved my core data dependency from cocoapods to swift package manager.

But here was my solution:

  1. add @objc(ManagedObjectName) to all of my NSManagedObject classes
  2. in the core data model editor, in the Data model inspector, delete Current Project Module and use the default for the Module configuration.
  3. As the above answers are saying, make sure to use Bundle.module instead of Bundle.main when loading your NSManagedObjectModel.
  4. I rewrote our core data stack to use NSPersistentStore instead of manually setting everything up (NSPersistentStoreCoordinator, NSManagedObjectContext, etc...) as per WWDC 2018 core data best practices

A few things to note:

The problem wasn't because of the wrong bundle, I had already applied that change when migrating over to SPM. For some reason the app just couldn't find any of my NSManagedObject classes at runtime.

Try #1 and #2 before you try #4. I have no idea if #4 helped with this issue or not as the app stopped crashing when I removed Current Project Module. It definitely cleaned up a lot of ugly legacy code though.