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?
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.
Dans le contexte des tests unitaires avec un magasin en mémoire, vous vous retrouvez avec deux modèles différents chargés:
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.
J'ai rencontré ce problème lorsque j'ai essayé de faire des tests unitaires liés à CoreData avec les objectifs suivants:
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:
setUp
des sous-classes XCTestCase qui tentent de recréer NSPersistentContainerIl faut donc résoudre ce problème en deux temps.
Vous pouvez ajouter un indicateur underTesting
pour déterminer s'il faut le configurer ou non.
managedObjectModel
qu'une seule fois dans tous les tests unitairesJ'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.
J'ai corrigé mes avertissements en modifiant les éléments suivants:
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: