多个NSEntityDescriptions声称NSManagedObject子类

77

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

}

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


我在App delegate的didFinishLaunchingWithOptions中获取了实际的托管上下文。而在我的测试中,我获取了一个内存上下文。当你运行一个测试时,didFinishLaunchingWithOptions将被调用。所以,我最终得到了两个上下文,这就是导致这些消息的原因。因此,在didFinishLaunchingWithOptions中检查是否正在运行测试。您可以检查进程环境中的XCInjectBundleInto键。 - Gene De Lisa
我在这个帖子中修复了这个问题:https://dev59.com/O1QK5IYBdhLWcg3wJMq0#72161126 - André Henrique da Silva
12个回答

70

后自动缓存

使用NSPersistent[CloudKit]Container(name: String),应该不再发生这种情况,因为它似乎现在会自动缓存模型(Swift 5.1,Xcode11,iOS13/MacOS10.15)。

前自动缓存

NSPersistentContainer/NSPersistentCloudKitContainer有两个构造函数:

第一个只是一个便捷初始化器,用于从磁盘加载模型并调用第二个构造函数。问题在于,在同一app/test调用内从磁盘中加载相同的NSManagedObjectModel两次会导致上述错误,因为每次加载模型都会引起外部注册调用,一旦在同一app/test调用上第二次调用就会打印错误信息。 而且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))
    }
}

旧答案

加载Core Data有点像魔术,从磁盘加载模型并使用它意味着它会注册某些类型。第二次加载尝试再次为该类型注册,这显然告诉你已经有东西注册了该类型。

您只能加载一次Core Data,每个测试后清理该实例。清理意味着删除每个对象实体,然后保存。有一些函数可以为您提供所有实体,然后可以获取和删除它们。批量删除不适用于内存中,因此需要通过逐个管理对象进行处理。

(可能更简单的)替代方法是加载模型一次,将其存储在某个位置上,并在每个NSPersistentContainer调用时重复使用该模型,它具有使用给定模型而不是重新从磁盘加载它的构造函数。

新答案

加载 Core Data 类型等同于魔术,从磁盘加载数据模型并使用后,就会为其注册某些类型。如果再次尝试加载相同类型,则说明已有某些内容对其进行了注册。

您只能加载一次 Core Data 并在每个测试后清理这个实例。清理意味着删除每个对象实体并进行保存。有一个函数可以提供所有实体,然后可以获取并删除它们。但不支持批量删除内存中的内容,需要逐个管理对象进行删除。

另一个(可能更简单)的选择是只需加载一次数据模型,将其存储在某个位置,并在每个NSPersistentContainer调用时重复使用该模型。它有一个构造函数可用于使用给定的模型而不必重新从磁盘加载它。


2
你所说的每次测试后清理实例是什么意思?我试图在每次测试后销毁持久化存储,但很快意识到,在内存存储中没有可用于销毁的URL。 - Nick Kohrn
2
这在我进行一些单元测试案例时非常有帮助,当时我正在设置CoreData Stack。将其移出以便只创建一次(因此只加载一次),问题得到了解决! - Sepui
6
@Sepui 你把它移动到哪里了?你是将它从 setup() 函数移动到了其他函数中吗? - Nick Kohrn
感谢您提供的解决方案和解释! - Kraig Wastlund
3
我花了很多时间来弄清楚这个问题。你解决了我的困难!将我的“NSManagedObjectModel”设置为所有测试的单例,并且每个测试函数从单例模型创建一个新的“NSPersistentContainer”实例,问题得以解决。我写了一份文档,试图概述我在使用CoreData时遇到的许多问题:https://gist.github.com/levibostian/a7d46afec7e5cd72eadaadb2dcf7a227 - levibostian
3
即使在iOS14.5 / XCode版本13.2.1,仍需要这样的解决方法。 - Cheok Yan Cheng

39
在使用内存存储的单元测试中,你会发现加载了两个不同的模型:
- 应用程序主 Core Data 栈加载的模型 - 内存堆栈的单元测试加载的模型
这会导致问题,因为显然 + [NSManagedObjectModel entity] 会查找所有可用的模型以找到匹配 NSManagedObject 的实体。由于它找到了两个模型,它会报错。
解决方法是使用 insertNewObjectForEntityForName:inManagedObjectContext: 将对象插入上下文中。这将考虑上下文(因此也考虑上下文的模型)来查找实体模型,并因此将其搜索限制为单个模型。
对我来说,这似乎是 NSManagedObject init(managedObjectContext:) 方法中的一个错误,它似乎依赖于 +[NSManagedObject entity] 而不是依赖于上下文的模型。

2
我曾采用核心数据堆栈的方法,使用sqlite将其保存到主应用程序的磁盘中。而对于单元测试,我则使用了内存堆栈。这两个堆栈都被加载以执行单元测试。当我需要运行单个测试时,它可以正常工作,但是当我有多个测试时,就会出现错误。通过实施解决方案,我的问题得到了解决。谢谢。 - Danilo Gomes
仅供参考,在修复之前,我遇到了相同的控制台错误,并且在context.save()时出现了以下错误:"操作无法完成。(Cocoa错误134020。)" - Danilo Gomes
11
好的,半天的调试后才发现 NSManagedObject 的 init(managedObjectContext:) 方法不能正常工作。 - undsoft

28

正如@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


2
谢谢你提供这个“扩展”,它帮了我很多忙。你犯了一个小错误,你在init的参数中命名为“usedContext”,但是在函数体中使用的是“context”;) - Kamil Harasimowicz

13
[错误] 警告: 多个 NSEntityDescriptions 声称是同一托管对象子类所引起的。 在 Core Data 单元测试上下文中,这种警告并不是很严重,因为我们知道它不会破坏任何东西。但是,通过添加一个静态托管对象模型并将其用于创建的每个持久性容器,也很容易摆脱警告消息。下面的代码片段中的 xcdatamodeld 是您的 Core Data 模型文件的文件名。 下面的代码片段基于 Xcode 生成的 Core Data 模板代码
public class PersistentContainer: NSPersistentCloudKitContainer {}

class PersistenceController {
    static let shared = PersistenceController()
    
    static var managedObjectModel: NSManagedObjectModel = {
        let bundle = Bundle(for: PersistenceController.self)
        
        guard let url = bundle.url(forResource: "xcdatamodeld", withExtension: "momd") else {
            fatalError("Failed to locate momd file for xcdatamodeld")
        }
        
        guard let model = NSManagedObjectModel(contentsOf: url) else {
            fatalError("Failed to load momd file for xcdatamodeld")
        }
        
        return model
    }()

    let container: PersistentContainer

    init(inMemory: Bool = false) {
        container = PersistentContainer(name: "xcdatamodeld", managedObjectModel: Self.managedObjectModel)
        
        if inMemory {
            container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
        }
        
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
    }
}

11

我在尝试使用CoreData进行单元测试时遇到了这个问题,目标如下:

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

正如Fabian所说,这个问题的根本原因是managedObjectModel被多次加载。但是,可能有几个可能的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
    }
}

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


3
我通过以下方式解决了我的警告:
  • 我在我的应用程序中两次加载持久存储,导致出现这些警告。我通过更改来解决了这个问题。
  • 如果您正在处理NSManagedObjectModel的内容,请确保使用来自persistentStoreCoordinatorpersistentStoreContainer的模型。之前,我直接从文件系统加载它并收到了警告。

我无法解决以下警告:

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

3
我通过在CoreData管理器类上将ManagedObjectModel公开为类属性来解决了这个问题:
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
}()

这样做的好处是不需要直接将母类或实体类添加到测试包中,这在以前是我发现需要做的事情。

1
我在内存单元测试中遇到了批量插入时的问题。我改用实体名称而不是实际实体的构造函数,这样就消除了警告。
我使用了以下代码:
NSBatchInsertRequest(entityName: entityNameAlert(), objects: ...) //<- entityNameAlert() is a method that returns my entity name as a string

改为:

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

我在内存存储中使用batchDelete时遇到了问题,但通过使用上述提供的扩展程序创建对象,我成功消除了这个问题:

接受的答案,但添加了扩展程序


1
请检查您的数据模型文件,同时加载指向持久容器相同位置并引用相同上下文的同一Core data类是完全可以的。
就像:modelForSaveDate和modelForRetrieveData,这两个可能在单个测试方法中指向同一个Coredata模型。
只需使用“representedClassName”属性检查您的数据模型文件源代码即可。
在我的情况下,“representedClassName”值奇怪地被附加了.(点)。 当我用新模型替换时,问题得到解决,因为现在“representedClassName”值没有附加.(点)。 这拯救了我的生命。 也许对你有帮助。

1

我访问了persistentContainer两次。我删除了其中一个。这修复了警告并且现在正常工作。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接