Несколько требований 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:

  1. В приложении
  2. В тестовых случаях каждый setUp setUp подклассов XCTestCase, которые пытаются воссоздать NSPersistentContainer

Так что в два раза решить эту проблему.

  1. Не устанавливайте стек NSPersistentContainer в приложении.

Вы можете добавить флаг underTesting чтобы определить, устанавливать его или нет.

  1. Загружать 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)