多个NSEntityDescriptions声明NSManagedObject子类

时间:2018-08-15 00:44:56

标签: core-data nsmanagedobject nsmanagedobjectcontext

我正在创建一个允许使用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)
    }

}

我在做什么导致测试中的错误?

11 个答案:

答案 0 :(得分:13)

在带有内存存储的单元测试的上下文中,最终会加载两个不同的模型:

  • 主Core Data堆栈在应用程序中加载的模型
  • 单元中加载的模型针对内存中的堆栈进行测试。

这会导致问题,因为显然<TargetLatestRuntimePatch>会查看所有可用的模型来为您的NSManagedObject查找匹配的实体。由于它找到两个模型,因此会抱怨。

解决方案是使用+ [NSManagedObjectModel entity]在上下文中插入对象。这将考虑上下文(以及上下文的模型)以查找实体模型,从而将其搜索限制为单个模型。

对我来说,这似乎是insertNewObjectForEntityForName:inManagedObjectContext:方法中的一个错误,该方法似乎依赖于NSManagedObject init(managedObjectContext:)而不是依赖于上下文的模型。

答案 1 :(得分:7)

加载核心数据有点神奇,从磁盘加载模型并使用它意味着它注册了某些类型。第二次加载会尝试再次注册该类型,这显然告诉您已经为该类型注册了某些东西。

您只能加载一次核心数据,并在每次测试后清理该实例。清理意味着删除每个对象实体,然后保存。有一些功能可以为您提供所有实体,然后您可以提取和删除它们。批量删除在InMemory中不可用,但是它是按对象管理的对象。

(可能更简单)的替代方法是一次加载模型,将其存储在某个位置,然后在每次NSPersistentContainer调用中重用该模型,它具有构造函数,可以使用给定的模型而不是从磁盘再次加载。 / p>

答案 2 :(得分:4)

@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)
    }

}

我找到了here,因此应将全部信用归功于@shaps

答案 3 :(得分:3)

我通过将ManagedObjectModel作为CoreData管理器类的类属性来解决此问题:

class PersistenceManager {
    let storeName: String!

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

    ...
}

...然后,在我的测试中,当我设置PersistentContainer时,我直接引用了该模型:

lazy var inMemoryContainer: NSPersistentContainer = {
    // Reference the model inside the app, rather than loading it again, to prevent duplicate errors
    let container = NSPersistentContainer(name: "TestContainer", managedObjectModel: PersistenceManager.managedObjectModel)
    let description = NSPersistentStoreDescription()
    description.type = NSInMemoryStoreType
    description.shouldAddStoreAsynchronously = false

    container.persistentStoreDescriptions = [description]
    container.loadPersistentStores { (description, error) in
        precondition(description.type == NSInMemoryStoreType)
        if let error = error {
            fatalError("Create an in-memory coordinator failed \(error)")
        }
    }
    return container
}()

这还有一个好处,就是不需要将妈妈类或实体类直接添加到测试包中,我发现我以前需要这样做。

答案 4 :(得分:3)

<块引用>

[error] 警告:多个 NSEntityDescriptions 声明 ...

此警告是由声明为同一托管对象子类的多个托管对象模型引起的。

在核心数据单元测试的上下文中,这没什么大不了的,因为我们知道它会破坏任何东西。但是,通过添加静态托管对象模型并将其用于我们创建的每个持久容器,也很容易消除警告消息。下面代码片段中的 gatsby-plugin-typography 是 Core Data 模型文件的文件名。

以下代码片段基于 Xcode 生成的 Core Data 模板代码

xcdatamodeld

答案 5 :(得分:2)

我尝试进行具有以下目的的CoreData相关单元测试时遇到了这个问题:

  • 内存类型NSPersistentContainer堆栈以提高速度
  • 为每个测试用例重新创建堆栈以擦除数据

作为Fabian的答案,此问题的根本原因是managedObjectModel被多次加载。但是,托管对象模型的加载可能有几个可能的地方:

  1. 在应用程序中
  2. 在测试案例中,每个XCTestCase子类的setUp调用都试图重新创建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
    }
}

以上内容足以解决单元测试中出现的问题。

答案 6 :(得分:1)

只需使用singleton一次创建您的managedContext,然后再使用它即可。它帮助我解决了同样的问题。

class CoreDataStack {

    static let shared = CoreDataStack()

    private init() {}

    var managedContext: NSManagedObjectContext {
        return self.storeContainer.viewContext
    }

//...
}

答案 7 :(得分:1)

我在 BatchInsert 内存单元测试中得到了这个。我改用构造函数作为实体名称而不是实体实际实体,这消除了警告。

我用过这个:

NSBatchInsertRequest(entityName: entityNameAlert(), objects: ...) //<- entityNameAlert() is a method that returns my entity name as a string

代替:

NSBatchInsertRequest(entity: Alert.entity(), objects: ...)

此外,我在内存存储中的 batchDelete 上获得了它,我能够通过使用上面提供的扩展创建对象来消除它:

accepted answer but added extension

答案 8 :(得分:0)

我通过更改以下内容来修正警告:

  • 我两次在我的应用程序中加载一个持久性存储,这导致了这些警告。
  • 如果您要在NSManagedObjectModel上进行操作,请确保您使用的是persistentStoreCoordinatorpersistentStoreContainer中的模型。在我直接从文件系统加载它并得到警告之前。

我无法修复以下警告:

  • 之前,我在应用程序生命周期中删除了我的整个持久存储并创建了一个新容器。我无法找出如何解决此后收到的警告。

答案 9 :(得分:0)

当对象模型有多个实例时,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)

答案 10 :(得分:0)

我已经两次访问了persistentContainer,我删除了一次,它修复了警告并可以正常工作。