Let me outline the relevant code:
I have a DataManager class as follows:
enum DataManagerType {
case normal, preview, testing
}
class DataManager: NSObject, ObservableObject {
static let shared = DataManager(type: .normal)
static let preview = DataManager(type: .preview)
static let testing = DataManager(type: .testing)
@Published var todos = [Todo]()
fileprivate var managedObjectContext: NSManagedObjectContext
private let todosFRC: NSFetchedResultsController<TodoMO>
private init(type: DataManagerType) {
switch type {
case .normal:
let persistentStore = PersistentStore()
self.managedObjectContext = persistentStore.context
case .preview:
let persistentStore = PersistentStore(inMemory: true)
self.managedObjectContext = persistentStore.context
for i in 0..<10 {
let newTodo = TodoMO(context: managedObjectContext)
newTodo.title = "Todo \(i)"
newTodo.isComplete = false
newTodo.date = Date()
newTodo.id = UUID()
}
try? self.managedObjectContext.save()
case .testing:
let persistentStore = PersistentStore(inMemory: true)
self.managedObjectContext = persistentStore.context
}
let todoFR: NSFetchRequest<TodoMO> = TodoMO.fetchRequest()
todoFR.sortDescriptors = [NSSortDescriptor(key: "date", ascending: false)]
todosFRC = NSFetchedResultsController(fetchRequest: todoFR,
managedObjectContext: managedObjectContext,
sectionNameKeyPath: nil,
cacheName: nil)
super.init()
// Initial fetch to populate todos array
todosFRC.delegate = self
try? todosFRC.performFetch()
if let newTodos = todosFRC.fetchedObjects {
self.todos = newTodos.map({todo(from: $0)})
}
}
func saveData() {
if managedObjectContext.hasChanges {
do {
try managedObjectContext.save()
} catch let error as NSError {
NSLog("Unresolved error saving context: \(error), \(error.userInfo)")
}
}
}
private func fetchFirst<T: NSManagedObject>(_ objectType: T.Type, predicate: NSPredicate?) -> Result<T?, Error> {
let request = objectType.fetchRequest()
request.predicate = predicate
request.fetchLimit = 1
do {
let result = try managedObjectContext.fetch(request) as? [T]
return .success(result?.first)
} catch {
return .failure(error)
}
}
}
My persistence store is as such:
struct PersistentStore {
let container: NSPersistentContainer
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "CoreDataModel")
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.viewContext.automaticallyMergesChangesFromParent = true
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
}
var context: NSManagedObjectContext { container.viewContext }
func saveContext () {
if context.hasChanges {
do {
try context.save()
} catch let error as NSError {
NSLog("Unresolved error saving context: \(error), \(error.userInfo)")
}
}
}
}
I get an error when calling the following in an XCTest:
let predicate = NSPredicate(format: "id = %@", todo.id as CVarArg) //todo.id is just some UUID() //irrelevant here
let result = fetchFirst(TodoMO.self, predicate: predicate)
This is the error I get:
2022-07-09 21:36:17.425709-0400 CoreDataExample[73965:7495035] [error] error: No NSEntityDescriptions in any model claim the NSManagedObject subclass 'CoreDataExampleTests.TodoMO' so +entity is confused. Have you loaded your NSManagedObjectModel yet ? CoreData: error: No NSEntityDescriptions in any model claim the NSManagedObject subclass 'CoreDataExampleTests.TodoMO' so +entity is confused. Have you loaded your NSManagedObjectModel yet ? 2022-07-09 21:36:17.425780-0400 CoreDataExample[73965:7495035] [error] error: +[CoreDataExampleTests.TodoMO entity] Failed to find a unique match for an NSEntityDescription to a managed object subclass CoreData: error: +[CoreDataExampleTests.TodoMO entity] Failed to find a unique match for an NSEntityDescription to a managed object subclass /Users/santiagogarciasantos/Documents/Xcode Projects/CoreDataExample/CoreDataExample/DataManager/DataManager.swift:87: error: -[CoreDataExampleTests.CoreDataExampleTests test_Add_Todo] : executeFetchRequest:error: A fetch request must have an entity. (NSInvalidArgumentException)
Following other solutions on here, I've checked the target, made sure my Entity is in "Current Product Module" but it still won't work.
Important: This only occurs when I'm in using XCTest (using my DataManager.testing), not in Previews or in simulator
Here's a link to a demo project I've made which recreates the issue.
Thanks for your help!
I made a couple of changes to your code to make the tests work:
you should not add your app classes to the test target. They are imported automatically by selecting a "Host Application" in you test target and made accessible by the directive @testable import CoreDataExample
.
when the test execute, an instance of CoreDataExampleApp
is created. This in turn instantiates var dataManager = DataManager.shared
.
Then the tests execute where you instantiate dataManager = DataManager.testing
.
Two instances of PersistentStore
are created which have their own version of the core data stack including the managedObjectModel
.
Those models are fighting over your NSManagedObject subclasses which results in objectType.fetchRequest()
having no entity.
To fix your issue, go through all files in your app target and make sure in FileInspector>TargetMembership only CoreDataExample is checked. Then in CoreDataExampleTests
change line 17 to dataManager = DataManager.shared
.
Your tests will run now.
If you want to keep your different DataManager
flavours, you have to make sure that only one instance of PersistentStore
is ever created. One simple way would be to make it static:
class DataManager: NSObject, ObservableObject {
static let persistentStore = PersistentStore()
// [...]
private init(type: DataManagerType) {
switch type {
case .normal:
self.managedObjectContext = DataManager.persistentStore.context
// etc.
Edit: Alternative Solution
The issue comes from using the TestTarget>General>'Host Application' feature in the first place, which Xcode sets now for new projects. This is meant for Application Tests where you need an app instance.
If you instead want to perform Logic Tests you should opt out of this feature. You then don't get the app instance with its side effects. In your case this is probably what you want. The tests will also run faster because the app does not need to be loaded.
But you then need to add all your files manually to your test target.
To use the alternative approach:
PersistentStore
finding the right model file by loading it from an explicit URL. Change this in your test class:
override func setUp() {
super.setUp()
let testBundle = Bundle(for: type(of: self))
let modelUrl = testBundle.url(forResource: "CoreDataModel", withExtension: "momd")
dataManager = DataManager(type: .testing, modelUrl: modelUrl)
}
Change your DataManager
and PersistentStore
accordingly to handle the optional URL:
// DataManager change not shown (just pass through)
struct PersistentStore {
init(inMemory: Bool = false, modelUrl: URL? = nil) {
if let url = modelUrl {
let mom = NSManagedObjectModel(contentsOf: url)! // todo: handle !
container = NSPersistentContainer(name: "CoreDataModel", managedObjectModel: mom) // todo: maybe get name from URL
} else {
container = NSPersistentContainer(name: "CoreDataModel")
}
I recommend this second approach, I consider it much cleaner.
If you also need Application Tests, add an additional target.