Несколько требований NSEntityDescription Подкласс NSManagedObject
Я создаю структуру, которая позволяет мне использовать Core Data. В целевой тестовой среде я настроил модель данных с именем MockModel.xcdatamodeld
. Он содержит единый объект с именем MockManaged
который имеет одно свойство Date
.
Чтобы я мог проверить свою логику, я создаю хранилище в памяти. Когда я хочу проверить свою логику сохранения, я создаю экземпляр хранилища в памяти и использую его. Тем не менее, я продолжаю получать следующий вывод в консоли:
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
Ниже приведен объект, который я использую для создания моих хранилищ в памяти:
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
}
}
Ниже приведено то, что составляет мой MockManaged
:
class MockManaged: NSManagedObject, Managed {
// MARK: - Properties
@NSManaged var date: Date
}
Ниже приведена моя 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)
}
}
Что я делаю, это вызывает ошибки в моих тестах?
Ответы
Ответ 1
После автоматического кэширования
Такого больше не должно случиться с NSPersistent[CloudKit]Container(name: String)
, так как кажется, что теперь он автоматически кэширует модель (Swift 5.1, Xcode11, iOS13/MacOS10.15).
Предварительное автоматическое кэширование
NSPersistentContainer/NSPersistentCloudKitContainer
имеет два конструктора:
Первый - это просто удобный инициализатор, вызывающий второй с загруженной с диска моделью. Проблема в том, что загрузка одного и того же NSManagedObjectModel
дважды с диска внутри одного и того же app/test invocation
приводит к ошибкам, указанным выше, поскольку каждая загрузка модели приводит к внешним регистрационным вызовам, которые при одной и той же ошибке [TG44 вызывают ошибки печати один раз. ].
И init(name: String)
был недостаточно умным, чтобы кэшировать модель.
Поэтому, если вы хотите загрузить контейнер несколько раз, вам нужно загрузить NSManagedObjectModel
один раз и сохранить его в атрибуте, который вы затем используете при каждом вызове init(name:managedObjectModel:)
.
Пример: кэширование модели
import Foundation
import SwiftUI
import CoreData
import CloudKit
class PersistentContainer {
private static var _model: NSManagedObjectModel?
private static func model(name: String) throws -> NSManagedObjectModel {
if _model == nil {
_model = try loadModel(name: name, bundle: Bundle.main)
}
return _model!
}
private static func loadModel(name: String, bundle: Bundle) throws -> NSManagedObjectModel {
guard let modelURL = bundle.url(forResource: name, withExtension: "momd") else {
throw CoreDataError.modelURLNotFound(forResourceName: name)
}
guard let model = NSManagedObjectModel(contentsOf: modelURL) else {
throw CoreDataError.modelLoadingFailed(forURL: modelURL)
}
return model
}
enum CoreDataError: Error {
case modelURLNotFound(forResourceName: String)
case modelLoadingFailed(forURL: URL)
}
public static func container() throws -> NSPersistentCloudKitContainer {
let name = "ItmeStore"
return NSPersistentCloudKitContainer(name: name, managedObjectModel: try model(name: name))
}
}
Старый ответ
Загрузка базовых данных - это немного волшебства, когда загрузка модели с диска и ее использование означает, что она регистрируется для определенных типов. Вторая загрузка снова пытается зарегистрироваться для типа, что, очевидно, говорит о том, что что-то уже зарегистрировано для типа.
Вы можете загрузить базовые данные только один раз и очищать этот экземпляр после каждого теста. Очистка означает удаление каждого объекта объекта, а затем сохранение. Существует некоторая функция, которая дает вам все сущности, которые вы можете затем извлечь и удалить. Пакетное удаление недоступно в InMemory, хотя объект, управляемый объектом, находится там.
(Вероятно, более простая) альтернатива - загрузить модель один раз, сохранить ее где-нибудь и повторно использовать эту модель при каждом вызове NSPersistentContainer
. У нее есть конструктор, который использует данную модель вместо того, чтобы снова загружать ее с диска.
Ответ 2
В контексте модульных тестов с хранилищем в памяти у вас загружаются две разные модели:
- Модель, загруженная в ваше приложение основным стеком Core Data
- Модель, загруженная в ваши юнит-тесты для стека в памяти.
Это вызывает проблему, потому что, по-видимому, + [NSManagedObjectModel entity]
просматривает все доступные модели, чтобы найти подходящую сущность для вашего NSManagedObject. Так как он находит две модели, он будет жаловаться.
Решение состоит в том, чтобы вставить ваш объект в контекст с помощью insertNewObjectForEntityForName:inManagedObjectContext:
Это будет учитывать контекст (и, как следствие, модель контекста) для поиска модели сущности и, как следствие, ограничивать ее поиск одной моделью.
Мне кажется, что это ошибка в методе NSManagedObject init(managedObjectContext:)
который, кажется, полагается на +[NSManagedObject entity]
а не на контекстную модель.
Ответ 3
Как указала @Kamchatka, предупреждение отображается потому, что используется NSManagedObject init(managedObjectContext:)
. Использование NSManagedObject initWithEntity:(NSEntityDescription *)entity insertIntoManagedObjectContext:(NSManagedObjectContext *)context
игнорирует это предупреждение.
Если вы не хотите использовать более поздний конструктор в своем тесте, вы можете просто создать расширение NSManagedObject
в своей цели теста, чтобы override
поведение по умолчанию:
import CoreData
public extension NSManagedObject {
convenience init(usedContext: NSManagedObjectContext) {
let name = String(describing: type(of: self))
let entity = NSEntityDescription.entity(forEntityName: name, in: usedContext)!
self.init(entity: entity, insertInto: usedContext)
}
}
Я нашел это здесь, так что полная заслуга должна идти в @shaps
Ответ 4
Я столкнулся с этой проблемой при попытке выполнить модульное тестирование, связанное с CoreData, со следующими целями:
- в памяти тип стека NSPersistentContainer для скорости
- пересоздать стек для каждого теста, чтобы стереть данные
Как отвечает Фабиан, коренной причиной этой проблемы является то, что managedObjectModel
загружается несколько раз. Однако может быть несколько возможных мест загрузки managedObjectModel:
- В приложении
- В тестовых случаях каждый
setUp
setUp подклассов XCTestCase, которые пытаются воссоздать NSPersistentContainer
Так что в два раза решить эту проблему.
- Не устанавливайте стек NSPersistentContainer в приложении.
Вы можете добавить флаг underTesting
чтобы определить, устанавливать его или нет.
- Загружать
managedObjectModel
только один раз во всех модульных тестах
Я использую статическую переменную для managedObjectModel
и использую ее для воссоздания в памяти NSPersistentContainer.
Некоторые выдержки как следующие:
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
}
}
Выше должно быть достаточно, чтобы решить эту проблему, возникающую при модульном тестировании.
Ответ 5
Я исправил свои предупреждения, изменив следующее:
- Я дважды загружал постоянное хранилище в своем приложении, которое привело к этим предупреждениям.
- Если вы делаете что-то в
NSManagedObjectModel
убедитесь, что используете модель из persistentStoreCoordinator
или persistentStoreContainer
. До того, как я загрузил его непосредственно из файловой системы и получил предупреждения.
Я не смог исправить следующие предупреждения:
- Ранее я удалил весь свой постоянный магазин и создал новый контейнер во время жизненного цикла приложения. Я не смог выяснить, как исправить предупреждения, которые я получил после этого.
Ответ 6
CoreData жалуется, когда есть несколько экземпляров объектных моделей. Лучшее решение, которое я нашел, это просто иметь место, где вы их статически определяете.
struct ManagedObjectModels {
static let main: NSManagedObjectModel = {
return buildModel(named: "main")
}()
static let cache: NSManagedObjectModel = {
return buildModel(named: "cache")
}()
private static func buildModel(named: String) -> NSManagedObjectModel {
let url = Bundle.main.url(forResource: named, withExtension: "momd")!
let managedObjectModel = NSManagedObjectModel.init(contentsOf: url)
return managedObjectModel!
}
}
Затем убедитесь, что при создании экземпляров контейнеров вы передаете эти модели явно.
let container = NSPersistentContainer(name: "cache", managedObjectModel: ManagedObjectModels.cache)