SaganRitual
SaganRitual

Reputation: 3203

Swift/OSX - How to give unit test Core Data access AND run the test without running the app

I'm using Core Data in my very small and simple app. It works fine. I'd like to set up some unit tests before I start growing the app and making it more complex. There are some hoops you have to jump through to get this to happen, but I've found numerous useful answers on SE to get me through those hoops. The basic idea is that you have to create the object model manually. Easy enough; I adapted this code from one of the answers linked above, and problem solved.

let managedObjectModel = NSManagedObjectModel.mergedModel(from: [Bundle.main])!
let persistentStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: managedObjectModel)
try! persistentStoreCoordinator.addPersistentStore(ofType: NSInMemoryStoreType, configurationName: nil, at: nil, options: nil)

let managedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
managedObjectContext.persistentStoreCoordinator = persistentStoreCoordinator

NSEntityDescription.entity(forEntityName: "VAgentEditor", in: managedObjectContext)!

Great. But I also want to run my tests without running the app, just to test my low-level logic. More hoops to jump through. Lots of useful answers on this too, problem solved.

The trouble happens when I try to access Core Data and run without the app window. Naively combining the solutions to both problems, I get nil back from the call to NSEntityDescription.entity(). I've tweaked the above code in every way I can think of, but to no avail. If I can hack it enough get non-nil back from the call to entity(), I end up getting messages that VAgentEditor can't be found, or when I try to save the context to disk, I get a file that looks like something worked halfway -- lots of stuff in it, but no VAgentEditor like I get when I try only one of the two working scenarios described above.

When I try to point the unit test to the main app's bundle, I get various complaints about the bundle not being found, or the unit test not being able to use both its own bundle and the app bundle, and a hundred other complaints that I don't remember after hacking at this for a couple of days.

Is there a way to get my unit test to use CoreData, access the correct bundle to get at the classes I defined in Interface Builder, and run the test without running the main app?

Upvotes: 1

Views: 152

Answers (1)

sundance
sundance

Reputation: 3040

Everything is easier, if you use CoreStore for database handling. You set up your database with this code:

let dataStack: DataStack = {
    let dataStack = DataStack(xcodeModelName: "ModelName")
    do {
        try dataStack.addStorageAndWait()
    } catch let error {
        XCTFail("Cannot set up database storage: \(error)")
    }
    return dataStack
}()

Assuming that you have the following database object:

class User: NSManagedObject {

    @NSManaged var name: String

    func rename(name: String, transaction: BaseDataTransaction?) {
        guard let user = transaction.edit(self) else {
            return
        }
        user.name = name
    }

}

You can run a unit test like this:

class FileTests: XCTestCase {

    func testRename() {
        // 1. Arrange
        var user: User?
        do {
            try dataStack.perform(synchronous: { transaction in
                user = transaction.create(Into<User>())
                user!.name = "Test"
            })
        } catch let error {
            XCTFail("Cannot perform database transaction: \(error)")
        }

        // 2. Action
        do {
            try dataStack.perform(synchronous: { transaction in
            guard let user = transaction.edit(user!) else {
                return
            }
            user.rename(name: "Renamed", transaction: transaction)
            })
        } catch let error {
            XCTFail("Cannot perform database transaction: \(error)")
        }

        // 3. Assert
        do {
            try dataStack.perform(synchronous: { transaction in
            guard let user = transaction.edit(user!) else {
                return
            }
            XCTAssertEqual(user.name, "Renamed")
            })
        } catch let error {
            XCTFail("Cannot perform database transaction: \(error)")
        }
    }

}

This is just the beginning. You can streamline and improve the whole process but I hope you get the idea.

Upvotes: 1

Related Questions