多个NSEntityDescriptions声明NSManagedObject子类

Nic*_*hrn 27 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
Run Code Online (Sandbox Code Playgroud)

下面是我用来创建内存存储的对象:

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
    }

}
Run Code Online (Sandbox Code Playgroud)

以下是构成我的MockManaged实体的内容:

class MockManaged: NSManagedObject, Managed {

    // MARK: - Properties

    @NSManaged var date: Date

}
Run Code Online (Sandbox Code Playgroud)

以下是我的意思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)
    }

}
Run Code Online (Sandbox Code Playgroud)

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

Fab*_*ian 17

自动缓存后

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

预自动缓存

NSPersistentContainer/NSPersistentCloudKitContainer 确实有两个构造函数:

第一个只是一个便利的初始化程序,第二个调用从磁盘加载的模型。问题在于,NSManagedObjectModel从磁盘中两次在同一磁盘中加载同一磁盘会app/test invocation导致上述错误,因为每次加载模型都会导致外部注册调用,而该调用会再次在同一磁盘上打印两次错误app/test invocation。而且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))
    }
}
Run Code Online (Sandbox Code Playgroud)

旧答案

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

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

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

  • @Sepui您将它移到哪里了?您是否将其从“ setup()”移至其他函数? (5认同)
  • 即使对于 iOS14.5/ XCode 版本 13.2.1 仍然需要这种解决方法 (3认同)
  • 确切地说,您在每次测试后清理实例是什么意思?我尝试在每次测试后销毁持久性存储,但是我很快意识到,尝试执行销毁时没有URL可使用,因为内存中的存储不使用存储URL。 (2认同)
  • 当我在某些 UnitTest 情况下在设置中初始化 CoreData 堆栈时,这对我有所帮助。将它移出它,以便它只创建一次(因此只加载一次)并修复它! (2认同)
  • 我花了很多时间试图弄清楚这一点。你拯救了这一天!使我的“NSManagedObjectModel”成为所有测试的单例,并从单例模型创建“NSPersistentContainer”的新实例,每个测试函数都修复了它。我制作了一个文档来尝试概述我在 CoreData 中遇到的许多问题:https://gist.github.com/levibostian/a7d46afec7e5cd72eadaadb2dcf7a227 (2认同)

Kam*_*tka 17

在使用内存存储的单元测试环境中,最终会加载两个不同的模型:

  • 主Core Data堆栈在您的应用程序中加载的模型
  • 单元中加载的模型测试内存堆栈.

这会导致问题,因为显然会+ [NSManagedObjectModel entity]查看所有可用模型以查找NSManagedObject的匹配实体.由于它找到两个模型,它会抱怨.

解决方案是将对象插入上下文中insertNewObjectForEntityForName:inManagedObjectContext:.这将考虑上下文(以及因此,上下文的模型)来查找实体模型,并因此将其搜索限制为单个模型.

对我来说,似乎是NSManagedObject init(managedObjectContext:)方法中的一个错误似乎依赖于+[NSManagedObject entity]而不是依赖于上下文的模型.

  • 很好,半天的调试只是为了发现NSManagedObject init(managedObjectContext :)不能正常工作。 (3认同)
  • 我正在使用将主要应用程序的sqlite保存到磁盘的核心数据堆栈的方法.对于单元测试,我有一个内存堆栈.正在加载两个堆栈以执行单元测试.当我必须运行单个测试时,它可以工作,但是当我进行多次测试时,我得到了错误.通过实施解决方案,它解决了我的问题.谢谢 (2认同)

Lor*_*lor 13

[错误]警告:多个 NSEntityDescriptions 声明...

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

在核心数据单元测试的背景下,这并不是什么大问题,因为我们知道它不会破坏任何东西。但是,通过添加静态托管对象模型并将其用于我们创建的每个持久容器,也可以轻松摆脱警告消息。xcdatamodeld下面的代码片段中是核心数据模型文件的文件名。

下面的代码片段基于 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)")
            }
        })
    }
}
Run Code Online (Sandbox Code Playgroud)


Krz*_*cki 6

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

}
Run Code Online (Sandbox Code Playgroud)

我在这里找到它,所以应该给@shaps充值

  • tnx 对于这个 `extension` 它帮助了我很多,你犯了一个小错误,你将 init 的参数命名为 `usedContext`,但你在正文中使用了 `context` ;) (2认同)

Egi*_* Li 5

尝试进行以下目标的CoreData相关单元测试时遇到了此问题:

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

作为Fabian的答案,此问题的根本原因是managedObjectModel多次加载。但是,托管对象模型的加载可能存在多个位置:

  1. 在应用程式中
  2. 在测试用例中,每次setUpXCTestCase子类调用都会尝试重新创建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
    }
}
Run Code Online (Sandbox Code Playgroud)

上面的内容足以解决您在单元测试中发生的问题。