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:

<your project>.xcdatamodeld’s Target Membership

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!

Recap

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 :)

References

Core Data Programming Guide: Persistent Store Types and Behaviors

iOS 10 Core Data In Memory Store for Unit Tests — Stack Overflow

Data Models and Model Objects

Real-World Testing with XCTest

Easier Core Data Setup with Persistent Containers

TestDouble