Adrian
Adrian

Reputation: 20058

How to use Core Data for main app and unit tests?

When I try to use Core Data with NSInMemoryStoreType for unit testing I always get this error:

Failed to find a unique match for an NSEntityDescription to a managed object subclass

This is my object to create the core data stack:

public enum StoreType {
    case sqLite
    case binary
    case inMemory
    .................
}

    public final class CoreDataStack {
        var storeType: StoreType!
        public init(storeType: StoreType) {
            self.storeType = storeType
        }

        lazy var persistentContainer: NSPersistentContainer = {
            let container = NSPersistentContainer(name: "Transaction")
            container.loadPersistentStores(completionHandler: { (description, error) in
                if let error = error {
                    fatalError("Unresolved error \(error), \(error.localizedDescription)")
                } else {
                    description.type = self.storeType.type

                }
            })

            return container
        }()

        public var context: NSManagedObjectContext {
            return persistentContainer.viewContext
        }

        public func reset() {
            for store in persistentContainer.persistentStoreCoordinator.persistentStores {
                guard let url = store.url else { return }

                try! persistentContainer.persistentStoreCoordinator.remove(store)
                try! FileManager.default.removeItem(at: url)
            }
        }
    }

And this is how I am using it inside my unit test project:

class MyTests: XCTestCase {

    var context: NSManagedObjectContext!
    var stack: CoreDataStack!

    override func setUp() {
        stack = CoreDataStack(storeType: .inMemory)
        context = stack.context
    }

    override func tearDown() {
        stack.reset()
        context = nil
    }
}

From what I read here which seems to be the same issue that I have, I have to cleanup everything after every test, which I (think) I am doing.

Am I not cleaning up correctly ? Is there another way to do this ?

Upvotes: 1

Views: 1358

Answers (2)

Neil Smith
Neil Smith

Reputation: 1051

I know this question is old, however, I've encountered this problem recently and didn't find an answer elsewhere.

Building on @JamesBedford 's answer, a way to setup your Core Data stack is:

  1. Ensure you only have a single instance of CoreDataStack in your app across both the app and test targets. Don't create new instances in your test target. In your app target, you could use a singleton as James suggests. Or, if you are keeping a strong reference to your Core Data stack in the AppDelegate and initialising at launch, provide a convenience static property in your app target to access from your test target. Something like:
extension CoreDataStack
    static var shared: CoreDataStack {
        (UIApplication.shared.delegate as! AppDelegate).stack
    }
}
  1. Add an environment variable to your test scheme in Xcode. Go to Xcode > Edit Scheme > Test > Arguments > Environment Variables. Add a new name-value pair such as: name = "persistent_store_type", value = "in_memory". Then, at runtime, inside your CoreDataStack initialiser, you can check for this environment variable using ProcessInfo.
final class CoreDataStack {
    
    let storeType: StoreType
    
    init() {
        if ProcessInfo.processInfo.environment["persistent_store_type"] == "in_memory" {
            self.storeType = .inMemory
        } else {
            self.storeType = .sqlLite
        }
    }
    
}

From here your test target will now use the .inMemory persistent store type and won't create the SQLLite store. You can even add a unit test asserting so :)

Upvotes: 1

James Bedford
James Bedford

Reputation: 28962

Is the CoreDataStack class initialised in your application? For instance, in an AppDelegate class? When the unit test is run it will initialise the AppDelegate some time before the test is ran. I believe this is so that your tests can call into anything from the app in order to test it, as per the line @testable import MyApp. If you're initialising a Core Data stack via your AppDelegate and in MyTests then you will be loading the Core Data stack twice.

Just to note, having two or more NSPersistentContainer instances means two or more NSManagedObjectModel instances will be loaded into memory, which is what causes the issue. Both models are providing additional NSManagedObject subclasses at runtime. When you then try to use one of these subclasses the runtime doesn't know which to use (even though they're identical, it just sees that they have the same name). I think it'd be better if NSManagedObjectModel could handle this case, but it's currently up to the developer to ensure there's never more than one instance loaded.

Upvotes: 1

Related Questions