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)
}
}
テストでエラーが発生する原因は何ですか?
これは、モデルを自動的にキャッシュするようになったため(Swift 5.1、Xcode11、iOS13/MacOS10.15)NSPersistent[CloudKit]Container(name: String)
ではもう発生しません。
_NSPersistentContainer/NSPersistentCloudKitContainer
_には2つのコンストラクターがあります。
1つ目は、ディスクからロードされたモデルで2つ目を呼び出す便利な初期化子です。問題は、同じNSManagedObjectModel
を同じ_app/test invocation
_内のディスクから2回ロードすると、上記のエラーが発生することです。モデルをロードするたびに外部登録呼び出しが発生し、2回目に呼び出されるとエラーが出力されます同じ_app/test invocation
_で。また、init(name: String)
wasは、モデルをキャッシュするのに十分ではありませんでした。
したがって、コンテナを複数回ロードする場合は、NSManagedObjectModel
を1回ロードして、属性に保存し、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))
}
}
_
コアデータのロードはちょっとした魔法です。ディスクからモデルをロードして使用するということは、特定のタイプに登録することを意味します。 2回目の読み込みでは、型の登録が再度試行されます。これにより、その型に既に登録されているものがあることが明らかにわかります。
Core Dataを一度だけロードし、各テスト後にそのインスタンスをクリーンアップできます。クリーンアップとは、すべてのオブジェクトエンティティを削除してから保存することです。取得して削除できるすべてのエンティティを提供する機能があります。 InMemoryではバッチ削除は使用できませんが、オブジェクトごとに管理されたオブジェクトが存在します。
(おそらくより簡単な)代替方法は、モデルを一度ロードして、どこかに保存し、NSPersistentContainer
呼び出しごとにそのモデルを再利用することです。
インメモリストアを使用した単体テストのコンテキストでは、2つの異なるモデルが読み込まれます。
明らかに_+ [NSManagedObjectModel entity]
_は利用可能なすべてのモデルを見て、NSManagedObjectに一致するエンティティを見つけるため、問題が発生します。 2つのモデルが見つかったため、文句を言います。
解決策は、_insertNewObjectForEntityForName:inManagedObjectContext:
_を使用してコンテキストにオブジェクトを挿入することです。これは、コンテキスト(および結果としてコンテキストのモデル)を考慮してエンティティモデルを検索し、結果として検索を単一のモデルに制限します。
私にとっては、NSManagedObject init(managedObjectContext:)
メソッドのバグのようで、コンテキストのモデルに依存するのではなく、_+[NSManagedObject entity]
_に依存しているようです。
@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)
}
}
次の目的でCoreData関連の単体テストを実行しようとしたときに、この問題が発生しました。
Fabianの答えとして、この問題の根本的な原因はmanagedObjectModel
が複数回ロードされることです。ただし、managedObjectModelをロードできる場所はいくつかあります。
setUp
呼び出ししたがって、この問題を解決するのは2つあります。
underTesting
フラグを追加して、セットアップするかどうかを決定できます。
managedObjectModel
を1回だけロードします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
}
}
この問題をユニットテストで修正するには、上記で十分です。
オブジェクトモデルのインスタンスが複数ある場合、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)
次を変更して警告を修正しました。
NSManagedObjectModel
で作業をしている場合は、persistentStoreCoordinator
またはpersistentStoreContainer
のモデルを使用していることを確認してください。ファイルシステムから直接ロードして警告を受け取る前に。次の警告を修正できませんでした: