skip to Main Content

This problem first appeared in my app project, but it’s baffled me enough that I tried to simplify things by switching to a Swift Package where I get the same issue.

In my project, I have a data model file called "Model.xcdatamodeld" (though the same problem appears no matter what the name). When I call the following init method in my running app, everything runs fine:

    init(title: String, then completion: @escaping ()->()) {
    self.container = NSPersistentContainer(name: title)
    
    container.loadPersistentStores { description, error in
        // don't worry about retaining self, this object lives the whole life of the app
        
        print(description)
        
        if let error = error {
            print("Error loading persistent store named (title): (error.localizedDescription)")
            return
        }
                    
        completion()
    }
}

No error is printed, the completion block is called, and I can access the persistent container’s viewContext for the rest of the life of the app.

But when I run the same code in my test target, I get very different behavior. The closure is never entered and when I try to access the container’s viewContext, it’s nil.

I’ve reduced the test to something that’s even simpler:

    func testLoadingMomD() {
    
    let expectation = XCTestExpectation(description: "core data model is set up")

    let container = NSPersistentContainer(name: "Model")
    container.loadPersistentStores { description, error in
        if let error = error {
            print(error.localizedDescription)
        }
        
        print(description)
        
        expectation.fulfill()

    }
    
    wait(for: [expectation], timeout: 10.0)

}

when I run this test, I get the following output in the console:

Test Case '-[GoldfishCoreDataTests.GoldfishCoreDataTests testLoadingMomD]' started.
2020-12-03 22:47:07.695319-0500 xctest[2548:72372] [error] error:  Failed to load model named Model
CoreData: error:  Failed to load model named Model
/Users/joseph/Documents/QuickScheduling/GoldfishCoreData/GoldfishCoreData/Tests/GoldfishCoreDataTests/GoldfishCoreDataTests.swift:23: error: -[GoldfishCoreDataTests.GoldfishCoreDataTests testLoadingMomD] : Asynchronous wait failed: Exceeded timeout of 10 seconds, with unfulfilled expectations: "core data model is set up".
Test Case '-[GoldfishCoreDataTests.GoldfishCoreDataTests testLoadingMomD]' failed (10.021 seconds).
Test Suite 'GoldfishCoreDataTests' failed at 2020-12-03 22:47:17.714.
     Executed 1 test, with 1 failure (0 unexpected) in 10.021 (10.021) seconds
Test Suite 'GoldfishCoreDataTests.xctest' failed at 2020-12-03 22:47:17.715.
     Executed 1 test, with 1 failure (0 unexpected) in 10.021 (10.022) seconds
Test Suite 'All tests' failed at 2020-12-03 22:47:17.715.
     Executed 1 test, with 1 failure (0 unexpected) in 10.021 (10.022) seconds
Program ended with exit code: 1

If I put a breakpoint anywhere within the closure, it never fires.

Of course, I’ve verified that the Model.xcdatamodeld file is in the appropriate target, and I’ve even deleted the Model.xcdatamodeld and recreated it with no change. As I said, this happens in my app project and in a separate Swift Package that only contains this test code and the Model.xcdatamodeld file.

For good measure, I have both an iOS and a macOS target in this project, and I get the same behavior no matter which target I test.

Is it just impossible to use NSPersistentContainer within an XCTest target?

2

Answers


  1. Chosen as BEST ANSWER

    So the short answer is that NSPersistentContainer only looks for its model file in the main bundle unless it's told otherwise. Try to use it in a target that's not an application target (like a test target) and it won't find the model file. You have to explicitly tell it which model file to use yourself. So you have to use Bundle to find the resource, which is of type "momd".

    Here's what I came up with:

    enum LoadingError: Error {
        case doesNotExist(String)
        case corruptObjectModel(URL)
    }
    
    init?(title: String, then completion: @escaping (Error?)->()) {
        
        guard let modelURL = Bundle(for: type(of: self)).url(forResource: title, withExtension: "momd") else {
            completion(LoadingError.doesNotExist(title))
            return nil
        }
    
        guard let managedObjectModel = NSManagedObjectModel(contentsOf: modelURL) else {
            completion(LoadingError.corruptObjectModel(modelURL))
            return nil
        }
        
        self.container = NSPersistentContainer(name: title, managedObjectModel: managedObjectModel)
        
        container.loadPersistentStores { description, error in
            // don't worry about retaining self, this object lives the whole life of the app
                        
            if let error = error {
                print("Error loading persistent store named (title): (error.localizedDescription)")
                DispatchQueue.main.async {
                    completion(error)
                }
                return
            }
                                   
            completion(nil)
        }
    }
    

    As an aside, apparently you can avoid this by subclassing NSPersistenContainer (see https://asciiwwdc.com/2018/sessions/224?q=nspersistentcontainer). This wasn't the preferred option for me for other reasons, but it may be useful for someone else.


  2. The problem is with the model and that it needs to be loaded from the right bundle. See this question for example.

    I have solved this by adding the following to my Core Data stack class (named CoreDataStore) where model is a static stored property

    private static var model: NSManagedObjectModel?
    
    enum ModelLoadError: Error {
        case loadFailed
    }
    private static func model(name: String) throws -> NSManagedObjectModel {
        if let oldModel = model {
            return oldModel
        }
    
        model = try loadModel(name: name)
        return model!
    }
    
    private static func loadModel(name: String) throws -> NSManagedObjectModel {
        guard let modelURL = Bundle.main.url(forResource: name, withExtension: "momd"),
            let model = NSManagedObjectModel(contentsOf: modelURL) else {
                throw ModelLoadError.loadFailed
        }
    
        return model
    }
    

    and then when creating the persistence container I do it like this

    do {
        let managedObjectModel = try CoreDataStore.model(name: modelName)
        self.persistentContainer = NSPersistentContainer(name: modelName,
                                                         managedObjectModel: managedObjectModel)
    } catch {
        fatalError("Failed to create persistence container with error: (error)")
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search