Cracking the Tests for Core Data
Core Data is a framework that hides the logic of persistent layers such as object life cycle and object graph management, to help you managing the model layer objects in a high-level way. Yes, Core Data is kind of controversial to many developers. However, it’s still useful framework: the performance is good and it’s supported by Apple.
There are many good articles on the Unit Testing for Core Data, but most of them are focusing on mocking the context. Due to the release of the persistent container class, NSPersistentContainer, it getting harder to mock the context and write tests to the container. Therefore, in this article I’m going to dig into a new topic: Writing tests for the NSPersistentContainer, enhanced Core Data.
Together we will go through set-upping Core Data stack with the NSPersistentContainer and writing tests for it.
We will learn how to:
- Build Core Data stack with NSPersistentContainer
- Use In-memory Persistent Store
- Introduce two test doubles: Fake and Stub
- Do an asynchronous tests
- Swift 3
- Xcode 8
- Basic knowledge of Core Data
- iOS 10 (since we will use NSPersistentContainer, iOS 10 is required).
Here are some good articles about Core Data:
- A Complete Core Data Application (the code is in Objective-C, but the concept is the same)
- An Introductory Core Data Tutorial
Now, we have a simple task: implement a todo list system, in which we can create, read, update and delete todos (CRUD). And this todo system should persistently save data so that we can fetch them later even if we terminate the app.
So we need to implement those methods:
- Create a todo
- Fetch all todos
- Remove a todo
- Commit changes to persistent store
Since we want to perform save only when needed, we are not going to treat NSManagedObjectContext.save() as a side effect in create/edit/remove methods. Instead, we make it an independent method. It will allow as to save data when a user presses “save” or save it when a user leaves the current view.
Although it’s easier to setup a Core Data stack after iOS 10 by using NSPersistentContainer, it would be good to have some basic knowledge about it. We are going to adopt these concepts in the following sections. Thus, in the next section, I will briefly introduce the Core Data stack.
Core Data stack
According to Apple, the Core Data stack is a collection of framework objects that are accessed as a part of the initialization of Core Data.
There are 4 basic components in the Core Data stack:
- Managed Object Context (NSManagedObjectContext): Provides a scratch pad for managed objects
- Persistent Store Coordinator (NSPersistentStoreCoordinator): Aggregates all the stores
- Managed Object Model (NSManagedObjectModel): Describes the entities in the stores
- Persistent Object Store: Contains saved records.
In this article, we mainly focus on Managed Object Context for concurrency, Persistent Store for the In-Memory persistent store, and Managed Object Model for sharing entity descriptions between the production target and the test target.
There used to be a lot of boilerplates when setting up a Core Data stack, but it became simpler after iOS 10. We are able to use NSPersistentContainer to encapsulate the whole Core Data stack with just a few lines of code.
Let’s step further now and setup a Core Data stack. If you select the Use Core Data option when you started the project, then you’re all set. If not, add the following snippet to AppDelegate to setup a global container:
A basic todo item should at least have two attributes:
- name (string)
- finished (bool)
So we create an entity named ToDoItem in managed object model (.xcdatamodeld):
Design a TodoStorageManager
The TodoStorageManager is a class that takes responsibilities for the interactions with the Core Data. With the help of this manager class, we can focus on other business logic without the interference of the storage logic and make storage code reusable.
Because it’s a class that mainly interacts with Core Data, the dependency of this class is the persistent container, without a doubt.
Here’s the initialization of the ToDoStorageManager:
Then, we want to commit changes in the background thread instead of main thread, so we setup a handy backgroundContext:
Finally, this is the implementation of CRUD and saving logic:
Now, we have a simple manager which is able to createdeletefetch todos and commits changes to the persistent store.
In this section, we gonna write the test code. We start with setting up the SUT:
We init a TodoStorageManager and inject a persistent container, mockPersistentContainer, as the dependency.
In the following two sections, we want to introduce two kinds of test doubles, Fake and Stub, to help us writing the test for Core Data. Bear with me, we are approaching our goal!
Fake — In-memory data store
For NSPersistentContainer, the default initializer creates a container with a persistent store type: NSSQLLiteStoreType. The problem is, we don’t want to use the real persistent database to store the data that generated by our test cases. We have to replace it.
Fakes: Objects actually have working implementations, but usually take some shortcut which makes them not suitable for production — Martin Fowler
Fake is a kind of test doubles, which has similar behaviors with the working environment. We usually use fake to simulate the production environments. In-memory database is a good example of the fake. An in-memory database has the similar behavior with the real persistent store, so we are able to put/fetch data from the in-memory database just as what we did in the real one.
For Core Data, there’s a special store type: NSInMemoryStoreType. When we setup a persistent store with type NSInMemoryStoreType, it will be an in-memory store. That is, if we terminate the app, the data in that store will be removed.
So, let’s setup our mock container with in-memory data store:
Now let’s go through the snippet line by line:
This line initializes a container with a customized managedObjectModel. If you don’t specify the managed object model, NSPersistentContainer picks up a managed model using the container’s name. It works well in the production target. The container can successfully find the right managed object model file if we give it the correct name. But in test target, the container can not automatically find the managed object model since the namespaces are different. So we have to assign the managed object model ourselves.
The following snippet describes a customized managed object model:
We create the model object from test Bundle. In order to make the model available in test Bundle, we also have to add the model file (.xcdatamodeld) to the test target:
We are almost all set for the container. Let’s take a look at the line below the initialization:
This is the key to use in-memory persistent store. Now the container in the test target has no more access to the production persistent store!
Stub — Canned responses
Stubs provide canned answers to calls made during the test, usually not responding at all to anything outside what’s programmed in for the test. — Martin Fowler
According to Martin Fowler, a stub is some kind of canned answer. With those canned answer, we can assert the correctness of our system with some predefined answers. In our case, since we have a fake store, we can easily setup an environment with some canned data in the store. Using those data as stubs we are able to assert something like the number of data or the content of the data.
So we put five items into database as stub objects:
In the test target, we are not able to use the autogenerated class, ToDoItem, to create a new object or fetch requests. So we back off to use the low-level way to create items. (update: It’s not a problem in the latest Xcode (9.2 in my experiment), you are able to use autogenerated class. Just remember to check the right target. :) )
We also want to make sure that every test case starts and ends with the same environment and condition. So we have to remove all stubs from the store after each test case:
Then we add that two methods to setUp() and tearDown():
So we have finished the setup of our stubs 🎉.
The Test Cases
Finally, we are able to write the test cases. According to our spec, we have the following 4 test-case drafts:
- insertTodoItem() should return ToDoItem
- fetchAll() should return correct number of ToDoItems
- remove() should remove an item from database
- save() should call NSManagedContext.save()
The test cases are rather simple, we follow the rules, given, when, and assert, to write test cases 1, 2, 3:
Since we are already know how many items in our database, we can just assert 5 items directly in the test_fetchall_todo(). It looks like some kind of integration tests instead of unit test. But it’s an efficient way to test Core Data when using NSPersistentContainer to build the stack.
Then, in the test_removetodo(), we put a side effect, save(), in the method. In this demo, we decided to do another integration test to simplify our story. After we call remove() to an item, it actually happens at background thread and won’t really affect the persistent store. If we want to assert the number of items in a database, we should commit the changes to the database as well.
Expectations to Notification
We still have one test case:
4. save() should call NSManagedContext.save()
The purpose of this case is to make sure data are committed to the database once we call ToDoStorageManager.save(). Generally, for a unit test we need to mock the context and assert the behavior of NSManagedObjectContext.save(). However, since we are using NSPersistentContainer, the context belongs to the container. It would be really tough to mock the context in the container.
But there’s still a way to assert the NSManagedObjectContext.save(). In Core Data, every change in certain context triggers the notification: NSManagedObjectContextDidSave. By observing the notification, we can easily assert that NSManagedContext.save() is called or not.
So we start to observe NSManagedObjectContextDidSave in setUp():
Let’s create a handler to this observation:
Then go back and write some code for test_save():
In this case, we create a todo and wait for NSManagedObjectContextDidSave. We need to let XCTestCase know what’s our expectation, so we set an expectation at expectation (description: “Context Saved”).
The expectation pattern is a test skill for testing asynchronous methods. We setup an expectation, and wait for the expectation to be fulfilled in a time slot.
expectation(description: “Context Saved”) is a predefined method on XCTestCase, which returns a XCTestExpectation object and adds the expectation to the waiting list. The expectation is fulfilled once the method, fulfill(), gets called.
Finally, we call waitForExpectations(timeout: TimeInterval, handler: XCTest.XCWaitCompletionHandler? = nil) to wait for all expectations to be fulfilled. If there’s still an expectation that is not fulfilled after timeout, the test fails.
The following snippet implements the waitForSavedNotification method:
We pass a handler and save the handler as a property. Now we are able to catch the NSManagedObjectContextDidSave:
Once we receive a NSManagedObjectContextDidSave, saveNotificationCompleteHandler will be triggered. It means that expect.fulfill() will be triggered as well. We are all set, all tests passed!
In this article, we have learnt:
- How to setup Core Data stack with NSPersistentContainer
- How to use Fake and Stub for Core Data
- How to test asynchronous requests in XCTestCase
The full code could be found on my GitHub.
There are good articles talking about the test for Core Data:
Apple actually makes it easy to setup the stack with the container. But it also introduces new problems on tests with NSPersistentContainer. In this article, we have found a way to test Core Data when using the NSPersistentContainer. Although it’s kind of complex, when it comes to doing test for Core Data, it still worth a shot!
Happy coding :)