XCTest + CoreData = ouch

I put this up in hopes that somebody runs across it more quickly than I did...

This weekend, as a "break", I decided to do some work updating an ancient (2003-vintage) piece of code that I wrote when I was doing extensive blogging. I'm not certain it'll ever leave my computers, but it was an opportunity to play around with some technologies that I'd honestly not touched in years, including CoreData.

Among the things that I did was modify my code to use the more modern NSPersistentContainer, in hopes that I could experiment some with CloudKit. Although it's likely that I'll do that manually, at least at first, the thought of trying out the latest way of doing this made sense (to me, at the time).

Unfortunately, I have a habit of writing unit tests. I say unfortunately not because I don't see enormous value in them (in fact, I uncovered a long-standing bug in the existing code with the first test that I wrote). I say unfortunately, because a lot of people don't write them, or don't take them as seriously, and that means that strange interactions between tests and other parts of the OS tend to be harder to find.

In this case, the NSPersistentContainer came back to bite me hard. As I later located, there are some other people who have seen this problem, and it caused some problems for them as well.

In my case, I'd already created a container for my CoreData stack, and that was part of what caused me the pain. In order to test that independently, I created a test bundle which executed stand-alone. This bundle (of course) had a copy of the model file. Here's where my problems started.

Due to things going on behind the scenes and differences between Objective-C's and Swift's handling of namespaces (oh, I didn't mention that I was also doing all the new code in swift, with an existing, albeit small, Objective-C code base?) I had to make sure that I was passing the managedObjectModel parameter to my NSPersistentContainer init method so that it would find the right one (otherwise in the test bundle it would fail entirely).

In order to do this, I needed code to grab the bundle of the class I was using so that I could guarantee that the right bundle was being loaded. Easy enough, I wrote a small class method:

static func managedObjectModel() -> NSManagedObjectModel {
    let bundle = Bundle(for: self)
    return NSManagedObjectModel.mergedModel(from: [bundle])!

and in my initializer for my container class, I loaded up the container:

let container = NSPersistentContainer(name: "SiteQuoter", managedObjectModel: SQExceptionManager.managedObjectModel())

Which worked great. For my first test; and on every subsequent test started throwing non-fatal warnings. Admittedly, I have a real problem with warning, both at compile time and run time and I don't like them in tests either, so I spent too much time trying to track this down. Note: the tests succeeded despite the warning that:

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

After looking around (see above links), I confirmed that other people were also seeing the unexpected caching of the NSManagedObjectModel and set about to make sure I only created one of them (hoping that would solve my NSPersistentContainer-related problem).

I won't disclose exactly how long it took to figure out the magic incantation, but I will disclose that Xcode's code analysis system was crashing most of the time that I wasn't doing it right, and thus "functionality was limited".

In the end, I replaced my static method with an lazily-initialized class variable:

static var managedObjectModel: NSManagedObjectModel = {
       let managedObjectModel = NSManagedObjectModel.mergedModel(from: [Bundle(for: SQExceptionManager.self)])!
       return managedObjectModel

Which is now working like a champ. Thanks to rennarda for their February answer of the aforementioned SO post.

I believe that the piece that threw me a number of times was the reference for the class in the Bundle(for:) call. For some reason, that regularly crashed the editor code and all the other solutions I tried (like referencing the class directly) failed. In the end, it seems as if the problem with referencing the class directly in that case had to do with something inferring that I was trying to call an initializer instead of the class.