web-dev-qa-db-fra.com

Plusieurs descriptions NSEntity réclament une sous-classe NSManagedObject

Je crée un cadre qui me permet d’utiliser Core Data. Dans la cible de test de la structure, j'ai configuré un modèle de données nommé MockModel.xcdatamodeld. Il contient une seule entité nommée MockManaged qui possède une seule propriété Date.

Pour pouvoir tester ma logique, je crée un magasin en mémoire. Lorsque je veux valider ma logique de sauvegarde, je crée une instance du magasin en mémoire et l’utilise. Cependant, je continue à recevoir la sortie suivante dans la console:

2018-08-14 20:35:45.340157-0400 xctest[7529:822360] [error] warning: Multiple NSEntityDescriptions claim the NSManagedObject subclass 'LocalPersistenceTests.MockManaged' so +entity is unable to disambiguate.
CoreData: warning: Multiple NSEntityDescriptions claim the NSManagedObject subclass 'LocalPersistenceTests.MockManaged' so +entity is unable to disambiguate.
2018-08-14 20:35:45.340558-0400 xctest[7529:822360] [error] warning:     'MockManaged' (0x7f986861cae0) from NSManagedObjectModel (0x7f9868604090) claims 'LocalPersistenceTests.MockManaged'.
CoreData: warning:       'MockManaged' (0x7f986861cae0) from NSManagedObjectModel (0x7f9868604090) claims 'LocalPersistenceTests.MockManaged'.
2018-08-14 20:35:45.340667-0400 xctest[7529:822360] [error] warning:     'MockManaged' (0x7f986acc4d10) from NSManagedObjectModel (0x7f9868418ee0) claims 'LocalPersistenceTests.MockManaged'.
CoreData: warning:       'MockManaged' (0x7f986acc4d10) from NSManagedObjectModel (0x7f9868418ee0) claims 'LocalPersistenceTests.MockManaged'.
2018-08-14 20:35:45.342938-0400 xctest[7529:822360] [error] error: +[LocalPersistenceTests.MockManaged entity] Failed to find a unique match for an NSEntityDescription to a managed object subclass
CoreData: error: +[LocalPersistenceTests.MockManaged entity] Failed to find a unique match for an NSEntityDescription to a managed object subclass

Voici l'objet que j'utilise pour créer mes magasins en mémoire:

class MockNSManagedObjectContextCreator {

    // MARK: - NSManagedObjectContext Creation

    static func inMemoryContext() -> NSManagedObjectContext {
        guard let model = NSManagedObjectModel.mergedModel(from: [Bundle(for: self)]) else { fatalError("Could not create model") }
        let coordinator = NSPersistentStoreCoordinator(managedObjectModel: model)
        do {
            try coordinator.addPersistentStore(ofType: NSInMemoryStoreType, configurationName: nil, at: nil, options: nil)
        } catch {
            fatalError("Could not create in-memory store")
        }
        let context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
        context.persistentStoreCoordinator = coordinator
        return context
    }

}

Voici ce qui constitue mon entité MockManaged:

class MockManaged: NSManagedObject, Managed {

    // MARK: - Properties

    @NSManaged var date: Date

}

Ci-dessous se trouve ce qui constitue ma XCTestCase:

class Tests_NSManagedObjectContext: XCTestCase {

    // MARK: - Object Insertion

    func test_NSManagedObjectContext_InsertsManagedObject_WhenObjectConformsToManagedProtocol() {
        let context = MockNSManagedObjectContextCreator.inMemoryContext()
        let changeExpectation = expectation(forNotification: .NSManagedObjectContextObjectsDidChange, object: context, handler: nil)
        let object: MockManaged = context.insertObject()
        object.date = Date()
        wait(for: [changeExpectation], timeout: 2)
    }

    // MARK: - Saving

    func test_NSManagedObjectContext_Saves_WhenChangesHaveBeenMade() {
        let context = MockNSManagedObjectContextCreator.inMemoryContext()
        let saveExpectation = expectation(forNotification: .NSManagedObjectContextDidSave, object: context, handler: nil)
        let object: MockManaged = context.insertObject()
        object.date = Date()
        do {
            try context.saveIfHasChanges()
        } catch {
            XCTFail("Expected successful save")
        }
        wait(for: [saveExpectation], timeout: 2)
    }

    func test_NSManagedObjectContext_DoesNotSave_WhenNoChangesHaveBeenMade() {
        let context = MockNSManagedObjectContextCreator.inMemoryContext()
        let saveExpectation = expectation(forNotification: .NSManagedObjectContextDidSave, object: context, handler: nil)
        saveExpectation.isInverted = true
        do {
            try context.saveIfHasChanges()
        } catch {
            XCTFail("Unexpected error: \(error)")
        }
        wait(for: [saveExpectation], timeout: 2)
    }

}

Qu'est-ce que je fais qui cause les erreurs dans mes tests?

13
Nick Kohrn

Le chargement de CoreData est un peu magique, car charger un modèle à partir d'un disque et l'utiliser signifie qu'il enregistre pour certains types. Un second chargement tente de réenregistrer le type, ce qui indique évidemment que sth est déjà enregistré pour le type.

Vous ne pouvez charger qu'une seule fois CoreData et nettoyer cette instance après chaque test. Le nettoyage signifie la suppression de chaque entité d'objet, puis sa sauvegarde. Il existe une fonction qui vous donne toutes les entités que vous pouvez ensuite récupérer et supprimer. La suppression par lot n’est pas disponible dans InMemory, bien que l’objet géré par l’objet soit présent.

L'alternative (probablement la plus simple) consiste à charger le modèle une fois, à le stocker quelque part et à le réutiliser à chaque appel NSPersistentContainer. Il dispose d'un constructeur qui utilise un modèle donné au lieu de le charger à nouveau depuis le disque.

4
Fabian

Dans le contexte des tests unitaires avec un magasin en mémoire, vous vous retrouvez avec deux modèles différents chargés:

  • Le modèle chargé dans votre application par la pile de données principale principale
  • Le modèle chargé dans votre unité teste la pile en mémoire.

Cela pose un problème car apparemment + [NSManagedObjectModel entity] examine tous les modèles disponibles pour trouver une entité correspondante pour votre NSManagedObject. Puisqu'il trouve deux modèles, il va se plaindre.

La solution consiste à insérer votre objet dans le contexte avec insertNewObjectForEntityForName:inManagedObjectContext:. Cela prendra en compte le contexte (et par conséquent, le modèle du contexte) pour rechercher le modèle d'entité et limitera par conséquent sa recherche à un seul modèle.

Pour moi, cela semble être un bogue dans la méthode NSManagedObject init(managedObjectContext:) qui semble s'appuyer sur +[NSManagedObject entity] plutôt que sur le modèle du contexte.

9
Kamchatka

J'ai rencontré ce problème lorsque j'ai essayé de faire des tests unitaires liés à CoreData avec les objectifs suivants:

  • pile en conteneur NSPersistentContainer de type en mémoire pour la vitesse
  • recréez une pile pour que chaque cas de test efface les données

Comme le dit Fabian, la cause principale de ce problème est que managedObjectModel est chargé plusieurs fois. Cependant, il peut y avoir plusieurs endroits possibles de chargement de managedObjectModel:

  1. Dans l'application
  2. Dans les cas de test, tous les appels setUp des sous-classes XCTestCase qui tentent de recréer NSPersistentContainer

Il faut donc résoudre ce problème en deux temps.

  1. Ne configurez pas la pile NSPersistentContainer dans l'application.

Vous pouvez ajouter un indicateur underTesting pour déterminer s'il faut le configurer ou non.

  1. Ne chargez managedObjectModel qu'une seule fois dans tous les tests unitaires

J'utilise une variable statique pour managedObjectModel et je l'utilise pour recréer NSPersistentContainer en mémoire.

Quelques extraits comme suit: 

class UnitTestBase {
    static let managedObjectModel: NSManagedObjectModel = {
        let managedObjectModel = NSManagedObjectModel.mergedModel(from: [Bundle(for: UnitTestBase.self)])!
        return managedObjectModel
    }()


    override func setUp() {
        // setup in-memory NSPersistentContainer
        let storeURL = NSPersistentContainer.defaultDirectoryURL().appendingPathComponent("store")
        let description = NSPersistentStoreDescription(url: storeURL)
        description.shouldMigrateStoreAutomatically = true
        description.shouldInferMappingModelAutomatically = true
        description.shouldAddStoreAsynchronously = false
        description.type = NSInMemoryStoreType

        let persistentContainer = NSPersistentContainer(name: "DataModel", managedObjectModel: UnitTestBase.managedObjectModel)
        persistentContainer.persistentStoreDescriptions = [description]
        persistentContainer.loadPersistentStores { _, error in
            if let error = error {
                fatalError("Fail to create CoreData Stack \(error.localizedDescription)")
            } else {
                DDLogInfo("CoreData Stack set up with in-memory store type")
            }
        }

        inMemoryPersistentContainer = persistentContainer
    }
}

Ci-dessus devrait suffire pour vous résoudre ce problème se produit dans les tests unitaires.

1
Egist Li

J'ai corrigé mes avertissements en modifiant les éléments suivants:

  • Je chargeais deux fois un magasin persistant dans mon application, ce qui entraîna ces avertissements.
  • Si vous utilisez des éléments sur NSManagedObjectModel, assurez-vous d'utiliser le modèle à partir de persistentStoreCoordinator ou persistentStoreContainer. Avant, je le chargeais directement du système de fichiers et je recevais des avertissements.

Je n'ai pas pu corriger les avertissements suivants:

  • Auparavant, j'avais supprimé tout mon magasin persistant et créé un nouveau conteneur au cours du cycle de vie de l'application. Je n'ai pas été en mesure de savoir comment corriger les avertissements que j'ai reçus après cela.
0
Gugmaster