If you’ve added your Core Data model to a Swift package and tried to load it from unit tests you’ve created in that same package it might surprise you when they fail.
Your package test target does not default to having access to the internal resources of the package. The solution is easier to explain with an example.
Adding Core Data To A Swift Package
I find it helpful to keep things like my apps model and related types in a separate Swift package that I then import into my app and other targets. The Swift package can then have unit tests that test the model in isolation from the App or any other package. Getting that to work is easier said than done.
Apple showed one way to setup the Core Data stack in a framework back in WWDC 2018. The Core Data Best Practices session creates a subclass of NSPersistentContainer
in the framework. Then the convenience initializer of NSPersistentContainer
should look for the managed object model in the bundle containing the subclass:
let container = CoreDataContainer(name: modelName)
I say should because I’ve had problems getting that to work with Swift packages. Either way it doesn’t help me as I keep my NSPersistentContainer
subclass in a separate package with other Core Data helper methods that are reusable across apps.
I pass the bundle containing the model file to my custom initializer which loads the MOM and calls the designated initializer of NSPersistentContainer
:
// CoreDataContainer.swift
public final class CoreDataContainer: NSPersistentContainer {
public init(name: String, bundle: Bundle = .main,
inMemory: Bool = false) {
guard let mom = NSManagedObjectModel.mergedModel(from: [bundle]) else {
fatalError("Failed to create mom")
}
super.init(name: name, managedObjectModel: mom)
configureDefaults(inMemory)
}
// ...
}
Then in my model package, I have a view model or data provider class that creates the Core Data stack using the package bundle (Bundle.module
):
public final class WorldStore: ObservableObject {
private let dataContainer: CoreDataContainer
public init(inMemory: Bool = false) {
dataContainer = CoreDataContainer(name: "Facts",
bundle: .module, inMemory: inMemory)
}
// ...
}
Since I never directly access the dataContainer
object outside of the package that works fine.
Adding Tests To The Package
There’s one small problem with this approach. If I add some Core Data tests to my Swift model package they don’t work:
// CoreDataTests.swift
final class CountryTests: XCTestCase {
private let modelName = "Facts"
private var container: CoreDataContainer!
override func setUpWithError() throws {
container = CoreDataContainer(name: modelName,
bundle: .module, inMemory: true)
}
The problem is that the tests can’t access the model resource file:
[error] error: No NSEntityDescriptions in any model claim the
NSManagedObject subclass 'Country' so +entity is confused.
Have you loaded your NSManagedObjectModel yet ?
The Bundle.module
I use in my tests returns the test bundle not the package bundle that contains the resources.
Accessing Package Resources
The problem is not with Core Data but with trying to access a resource from outside the package bundle. I mentioned this briefly in my post on adding resources to a package:
Note that resources don’t default to being accessible outside the package bundle. If you want to access a resource from outside the package module create a publicly visible property.
The test target is not in the same bundle as the package so cannot access the packages resources. One solution is to expose the package bundle as a public symbol:
// ModelKit.swift
public enum ModelKit {
public static let bundle = Bundle.module
}
Then in my Core Data model tests, instead of using .module
which refers to the test bundle, I can use the model package bundle:
// CoreDataTests.swift
container = CoreDataContainer(name: modelName,
bundle: ModelKit.bundle, inMemory: true)
That works but Apple recommends you don’t expose the whole resource bundle. Directly accessing the resources via the bundle creates a dependency that breaks if I ever change the internal resource names. I could instead create a public symbol for the URL of the momd
file:
// ModelKit.swift
public enum ModelKit {
public static let momURL = Bundle.module.url(forResource: "Facts",
withExtension: "momd")
}
That avoids exposing the name of the momd
file but I don’t need my Core Data model to be public. I only need to access if from the test target. I ended up keeping the symbol internal
:
// ModelKit.swift
internal enum ModelKit {
internal static let bundle = Bundle.module
}
Then I can use @testable
to allow my package tests to have access to internal
resources and my tests work:
// CoreDataTests.swift
@testable import ModelKit
// ...
container = CoreDataContainer(name: modelName,
bundle: ModelKit.bundle, inMemory: true)
Let me know if you have a better solution.