Core Data Unit Testing
Luke, 30 Oct 2018
How best to structure an app so that you can unit test with Core Data? Thats the question Ive tried to answer with the iOS app template available here.
Features
This working app template has the following features :
- In Memory Managed Persistent Store (and context) – just for the tests.
- An example of how this is used during a unit test
- ViewModel pattern, Dependency Injection and separation of concerns
In-Memory Persistent Store
The central feature of Core Data is the ability to store data in a persistent store that allows data to stick around permanently, ie after the app is no longer running. Importantly though, when unit testing, you don’t want to use that persistent data. You want to have a temporary persistent store separate to the real one that the app uses. For this, you need to use an In Memory Persistent Store, and you can see how those are created here, in the first class func.
Using this temporary store, along with the context created with it, allows you to create and delete objects, without affecting any real data that you have in the local installation of your app. Any data in the main apps persistent store would affect the outcome of your unit tests too, so this ensures that we have a clean, empty store to test with. And any creating or deleting of objects does not touch the real persistent store. You cant unit test Core Data without this (and if you are, it’s not going to work).
An example unit test
There is an example unit test available here. This swift file is supposed to contain all unit tests for the MainViewModel class. As such, there is a class property to contain an instance of MainViewModel, and also a managed object context created with an In-Memory Persistent Store, as described above.
During the unit test setup function, we pass this context into the MainViewModel to replace the one it would contain during normal running of the app. This is dependency injection in action : we can use all functions of the MainViewModel class, and the context used will be the one we set here, rather than the main context which would link to the apps main persistent store. In this way, we dont mess with that real persistent data during tests. This is explained further below.
So now we can test one of our MainViewModel function : createAnObject. It should do what it says on the tin – add an example object into our Core Data context. Having called this function any number of times, we should be able to do a fetch and find the same number of objects in Core Data, as the amount of times we called it. So in our test, I call it three times, then i check that there are 3 objects fetched back from Core Data afterwards.
func testCreateAnObject() {
//empty store of objects by deleting all. then add 3 object to it.
do {
try UnitTestHelpers.deleteAllObjects(objectType: ExampleObject.self, withContext: context)
try viewModel.createAnObject()
try viewModel.createAnObject()
try viewModel.createAnObject()
} catch {
if let vmerror = error as? ViewModelError {
print("error : \(vmerror.localizedDescription)")
}
XCTFail("Could not create objects")
}
//create a fetch request to retrieve all those objects
let fetchRequest = ExampleObject.fetchRequest() as NSFetchRequest
do {
let results = try context.fetch(fetchRequest)
XCTAssert(results.count == 3)
} catch {
XCTFail("Unabled to fetch objects")
}
}
Dependency Injection
Ive often seen apps that use a Singleton class for managing Core Data. It can make sense in some situations: you often need to call to save context for example, and its a pain to have to get a reference to the AppDelegate each time, which is where a standard Core Data app has its context property.
However, its not convenient to use Singletons when unit testing. They can by nature persist outside the current realms of the test in progress, and so might contain old data or states that could affect the current test. Theres a good argument about why singletons are not suitable for unit tests in this article.
So instead, we use dependency injection, and it’s just a much nicer way to do things. In the app template, our persistent store and managed object contexts are created in the AppDelegate. Then during appDidFinishLaunching , we get a reference to our MainViewController (and its accompanying ViewModel), and we set a pointer to that context. This means obviously, that throughout the MainViewModels operation, it can access the context easily by just using self.context. We can continue to propagate that context throughout the app by setting further pointers on subsequent UIViewControllers during segues.
guard let mainVC = self.window?.rootViewController as? MainViewController else {
fatalError("Unable to get access to main window")
}
let context = self.persistentContainer.viewContext
mainVC.viewModel.context = context
And so to the big bonus for testing : when we create an instance of our MainViewModel within the unit test, we just set that pointer to our new context, linked to the In Memory Persistent Store. So very easily, all Core Data work throughout the class does not affect the normal store, and just works with our in-memory one. And not a singleton in sight.
override func setUp() {
super.setUp()
//put our test context onto the viewmodel
viewModel.context = self.context
}
Generic fetching and sorting of Core Data objects
Finally, heres a handy method using generics that will fetch all objects of a given type, and sort them by a given property too. It’s the last class here. Typical usage is like this :
do {
let objects : [ExampleObject] = try UnitTestHelpers.fetchObjects(withContext: context, sortedBy: "", ascending: true)
} catch {
//handle failing that fetch
}
Replacing the ExampleObject with the object type from your own Core Data model is enough to tell the fetch function exactly what you are after. You can pass in nil if you don’t want the objects sorted, or pass in the name of the property you want to sort by as a String, along with true or false for ascending / descending.
Thats it : please let me know any feedback you might have on this approach to unit testing Core Data, and how it could be improved.