如何将 CoreData 暴露给 swift 包单元测试?

How to expose CoreData to swift package unit tests?

我正在尝试在我的 swift 包中测试 CoreData,因为 SPM 现在支持包括 .xcdatamodel 在内的捆绑资源,但我的测试似乎无法找到我的 NSManagedObjects。从测试中单元测试核心数据的步骤是什么?

当我尝试从测试创建 NSManagedObject 时出现此错误:

+entityForName: could not locate an entity named 'StriveUser' in this model. (NSInternalInconsistencyException)

我已经三重检查了命名,一切正确。

我正在通过测试创建这样的对象:

let object = NSEntityDescription.insertNewObject(forEntityName: "StriveUser", into: self.inMemoryStore.context)

这是我用于定位 .xcdatamodel 的代码:

    fileprivate var managedObjectModel: NSManagedObjectModel = {
    guard let managedObjectModel = NSManagedObjectModel.mergedModel(from: [Bundle.main]) else {
        preconditionFailure("Error getting ManagedObjectModel")
    }

    return managedObjectModel
}()

final class InMemoryStore {
    let context: NSManagedObjectContext

    init() {
        let description = NSPersistentStoreDescription()
        description.type = NSInMemoryStoreType
        description.shouldAddStoreAsynchronously = false

        let container = NSPersistentContainer(name: Constants.modelName, managedObjectModel: managedObjectModel)
        container.persistentStoreDescriptions = [description]
        container.loadPersistentStores {_, error in
            if let error = error {
                fatalError("Failed to load store: \(error.localizedDescription)")
            }
        }

        self.context = container.viewContext
    }
}

如果您在包清单中声明 Swift 工具版本 5.3 或更高版本,您可以将资源与源代码捆绑为 Swift 包。例如,Swift 包可以包含资产目录、故事板等。

定义资源后,将为包创建一个新的静态 Bundle 引用。这可以使用 Bundle.module.

访问

因此,对于您的 ManagedObjectModel,您需要更新包参考。使用它的一个好方法是在你的包中有一个访问器,它将 return 模型。

有关其他信息,您可以查看 Apple 的开发人员文档 Bundling Resources with a Swift Package

我 运行 遇到了类似的问题,每当我尝试对我的核心数据模型执行任何操作时,我的应用程序都会因“找不到 YourManagedObject”错误而崩溃。

当我将我的核心数据依赖项从 cocoapods 移动到 swift 包管理器时,这种情况就开始发生了。

但这是我的解决方案:

  1. @objc(ManagedObjectName) 添加到我所有的 NSManagedObject 类
  2. 在核心数据模型编辑器的数据模型检查器中,删除 Current Project Module 并使用模块配置的默认值。
  3. 正如上面的答案所说,确保在加载 NSManagedObjectModel 时使用 Bundle.module 而不是 Bundle.main。
  4. 我重写了我们的核心数据堆栈以使用 NSPersistentStore 而不是根据 WWDC 2018 核心数据最佳实践手动设置所有内容(NSPersistentStoreCoordinator、NSManagedObjectContext 等...)

注意几点:

问题不是因为错误的包,我在迁移到 SPM 时已经应用了那个更改。由于某种原因,应用程序在运行时找不到我的任何 NSManagedObject 类。

在尝试#4 之前先尝试#1 和#2。我不知道 #4 是否有助于解决这个问题,因为当我删除 Current Project Module 时应用程序停止崩溃。不过,它确实清理了很多丑陋的遗留代码。

我构建了一个新的 bare-bones Swift 包来演示似乎是导致这些问题的错误,截至 Xcode 版本 13.3.1 (13E500a)

No NSEntityDescriptions in any model claim the NSManagedObject subclass 'TestModel.EntityMO' so +entity is confused. Have you loaded your NSManagedObjectModel yet ?

加载模型后立即中断,我可以看到实体存在:

(lldb) po model
(<NSManagedObjectModel: 0x6000015f8d70>) isEditable 1, entities {
Entity = "(<NSEntityDescription: 0x6000001c4b00>) name Entity, managedObjectClassName TestModel_TestModel.EntityMO, 
<snip>

managedObjectClassName 似乎是问题所在。这是在模型中的 class 定义中使用 Current Product Module 的结果,它似乎将包 top-level 名称和源中包含的文件夹连接在一起。如果我将其替换为 TestModel 的 hard-coded 模块,则错误消失并且测试通过。不理想,但在我的情况下有效。

Xcode 对 swift 包中核心数据的支持似乎是 work-in-progress,因为编辑器仍然无法正确加载 .xcdatamodeld 文件。只是创建测试模型必须在另一个项目中完成并移动到包中,因为我无法将实体添加到空模型文件中。

作为参考,我还将包括我的模型初始化,这是非常基本的,但我相信相对于 Apple 指南而言是合理的。至少,它表明问题可能存在于 Bundle.module 使用之外。

public struct TestModel {
    internal static let modelURL = Bundle.module.url(forResource: "Model", withExtension: "momd")!

    public static func persistentContainer() -> NSPersistentContainer {
        let model = NSManagedObjectModel(contentsOf: modelURL)!
    
        let description = NSPersistentStoreDescription()
        description.type = NSInMemoryStoreType
        let container = NSPersistentContainer(name: "Test", managedObjectModel: model)
        container.persistentStoreDescriptions = [description]
    
        container.loadPersistentStores { storeDescription, error in
            guard error == nil else {
                fatalError("Could not load persistent stores. \(error!)")
            }
        }

        return container
    }
}