I have an issue with my Core Data setup for Unit Tests.
I use the default/new Core Data stack setup in my AppDelegate
class AppDelegate: UIResponder, UIApplicationDelegate {
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "GenericFirstFinder")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
}
And for testing I have a custom function to create a managed context
func setupInMemoryManagedObjectContext() -> NSManagedObjectContext {
let container = NSPersistentContainer(name: "GenericFirstFinder")
let description = NSPersistentStoreDescription()
description.type = NSInMemoryStoreType
container.persistentStoreDescriptions = [description]
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container.viewContext
}
I have an extension to find the first item given a predicate.
extension NSManagedObject {
class func first(with predicate: NSPredicate?, in context: NSManagedObjectContext) throws -> Self? {
return try _first(with: predicate, in: context)
}
fileprivate class func _first<T>(with predicate: NSPredicate?, in context: NSManagedObjectContext) throws -> T? where T: NSFetchRequestResult, T: NSManagedObject {
let fetchRequest = self.fetchRequest()
fetchRequest.fetchLimit = 1
fetchRequest.predicate = predicate
let results = try context.fetch(fetchRequest)
return results.first as? T
}
}
Here's the issue.
This test passes:
func testExample() {
let context = setupInMemoryManagedObjectContext()
let entity = try! Entity.first(with: nil, in: context)
XCTAssertEqual(entity, nil)
}
but if the persistentContainer loading is triggered in didFinishLaunchingWithOptions
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
let context = persistentContainer.viewContext // end of laziness
return true
}
then I get the following error
failed: caught "NSInvalidArgumentException", "executeFetchRequest:error: is not a valid NSFetchRequest."
The error comes specifically from this line in _first()
let fetchRequest = self.fetchRequest() // fetchRequest is <uninitialized> in that case
But if I modify the test to create an entity first, the test runs fine again (the XCTAssertEqual is different of course…):
func testExample() {
let context = setupInMemoryManagedObjectContext()
let firstEntity = Entity(context: context)
let entity = try! Entity.first(with: nil, in: context)
XCTAssertEqual(entity, firstEntity)
}
So for some reason, creating a new entity (without saving the context) seems to put things back in order.
I guess my stack setup for testing is screwed up, but I haven't figured out why. Do you understand what's happening and what is the proper way to set things up?
I'm using Xcode 8.3, Swift 3.1 and the deployment target is iOS 10.3.
Looks like this is a Core Data problem in the Unit test target (maybe also when generics are used). I also encountered it at work, this is a solution we have:
func fetchObjects<Entity: NSManagedObject>(type: Entity.Type,
predicate: NSPredicate? = nil,
sortDescriptors: [NSSortDescriptor]? = nil,
fetchLimit: Int? = nil) throws -> [Entity] {
/*
NOTE: This fetch request is constructed this way, because there is a compiler error
when using type.fetchRequest() or Entity.fetchRequest() only in the test target, which
makes the fetchRequest initialize to nil and crashes the fetch at runtime.
*/
let name = String(describing: type)
let fetchRequest: NSFetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: name)
I'm not familiar with the class function fetchRequest
and didn't know it existed until looking it up for this question.
I looked up some references but have to admit I still don't understand what's happening.
Recently I've been using the automatically generated classes that Xcode creates that for each entity includes a fetchRequest
method that looks like this:
@nonobjc public class func fetchRequest() -> NSFetchRequest<User> {
return NSFetchRequest<Entity>(entityName: "Entity")
}
Using that as a format I changed a line in your _first
function to this:
fileprivate class func _first<T>(with predicate: NSPredicate?, in context: NSManagedObjectContext) throws -> T? where T: NSFetchRequestResult, T: NSManagedObject {
// let fetchRequest = self.fetchRequest()
// (note that this wouldn't work if the class name and entity name were not the same)
let fetchRequest = NSFetchRequest<T>(entityName: T.entity().managedObjectClassName)
fetchRequest.fetchLimit = 1
fetchRequest.predicate = predicate
let results = try context.fetch(fetchRequest)
return results.first
}
Which for me prevents the error - I'm posting this to see if it helps you but I'm going to have to investigate some more to understand why this works.
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