web-dev-qa-db-ja.com

複数のNSEntityDescriptions要求NSManagedObjectサブクラス

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

}

テストでエラーが発生する原因は何ですか?

23
Nick Kohrn

自動キャッシング後

これは、モデルを自動的にキャッシュするようになったため(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呼び出しごとにそのモデルを再利用することです。

14
Fabian

インメモリストアを使用した単体テストのコンテキストでは、2つの異なるモデルが読み込まれます。

  • メインのコアデータスタックによってアプリケーションにロードされたモデル
  • ユニットにロードされたモデルは、メモリ内スタックをテストします。

明らかに_+ [NSManagedObjectModel entity]_は利用可能なすべてのモデルを見て、NSManagedObjectに一致するエンティティを見つけるため、問題が発生します。 2つのモデルが見つかったため、文句を言います。

解決策は、_insertNewObjectForEntityForName:inManagedObjectContext:_を使用してコンテキストにオブジェクトを挿入することです。これは、コンテキスト(および結果としてコンテキストのモデル)を考慮してエンティティモデルを検索し、結果として検索を単一のモデルに制限します。

私にとっては、NSManagedObject init(managedObjectContext:)メソッドのバグのようで、コンテキストのモデルに依存するのではなく、_+[NSManagedObject entity]_に依存しているようです。

15
Kamchatka

@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 に行くべきです

次の目的でCoreData関連の単体テストを実行しようとしたときに、この問題が発生しました。

  • 速度のためのメモリ内タイプNSPersistentContainerスタック
  • テストケースごとにスタックを再作成してデータを消去する

Fabianの答えとして、この問題の根本的な原因はmanagedObjectModelが複数回ロードされることです。ただし、managedObjectModelをロードできる場所はいくつかあります。

  1. アプリ内
  2. テストケースでは、NSPersistentContainerを再作成しようとするXCTestCaseサブクラスのすべてのsetUp呼び出し

したがって、この問題を解決するのは2つあります。

  1. アプリでNSPersistentContainerスタックを設定しないでください。

underTestingフラグを追加して、セットアップするかどうかを決定できます。

  1. すべての単体テストで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
    }
}

この問題をユニットテストで修正するには、上記で十分です。

4
Egist Li

オブジェクトモデルのインスタンスが複数ある場合、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)
0
Craig

次を変更して警告を修正しました。

  • アプリに永続ストアを2回ロードすると、これらの警告が表示されました。
  • NSManagedObjectModelで作業をしている場合は、persistentStoreCoordinatorまたはpersistentStoreContainerのモデルを使用していることを確認してください。ファイルシステムから直接ロードして警告を受け取る前に。

次の警告を修正できませんでした:

  • 以前、永続ストア全体を削除し、アプリのライフサイクル中に新しいコンテナを作成しました。この後に得た警告を修正する方法を見つけることができませんでした。
0
Gugmaster