Testing Core Data In A Swift Package

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.

Swift Package containing model and CoreDataController files

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.