Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

NSPersistentContainer & UnitTests with iOS10

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.

like image 639
Arnaud Avatar asked Apr 05 '17 12:04

Arnaud


2 Answers

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)
like image 144
Alistra Avatar answered Oct 09 '22 15:10

Alistra


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.

like image 41
MathewS Avatar answered Oct 09 '22 14:10

MathewS