Can Unit Testing and Core Data become BFFs?

Core Data and Unit Testing haven't always been the best of friends. Like two members of the same friend group who don't really know each other but really like their UIKit friend, Core Data and Unit Testing have in fact discovered that they have a lot in common and have gradually got more and more friendly with each other.

But before we delve into how they can take it one step further and become firm friends, we need to understand what makes each of them tick.

Getting to know each other

Core Data

Core Data is an object graph manager that helps you create the model layer of your app. An object graph is a collection of objects connected to each other through relationships. Core Data abstracts away the lifecycle of the objects it controls, providing tools to allow CRUD operations to be performed on those objects in its graph. In one configuration, Core Data's object graph can be persisted between executions of the app, with those objects being persisted in either XML, binary, or SQLite stores. However in another configuration, the object graph exists purely in-memory and dies when the app is killed. In fact, the range of configuration options available to Core Data makes it very difficult to define what Core Data actually is - in one configuration Core Data can resemble a SQL wrapper while in a different configuration it looks more like an ORM. The truth is that Core Data sits somewhere between both, with the ability to intelligently load a subset of its store's data into memory as custom domain objects that can be manipulated like any other Swift object before being "saved" back to the store where those changes are then persistent (be that across app executions or just in that app execution).

A consequence of this flexibility is it's not uncommon to see two apps using Core Data but in significantly different ways. This has led to Core Data gaining a reputation as a hard to use framework (not helped by the significant changes that the framework has gone through since inception). To avoid any confusion around what configuration of Core Data we are going to use, in this post our setup will look like:

Persistent Container is a fairly new member of the Core Data family. It was introduced in iOS 10 to help simplify the creation of the managed object model, persistent store coordinator and any managed object contexts.

Managed Object Context is a temporary, in-memory record of all NSManagedObject instances accessed, created or updated during its lifecycle. An app will typically have multiple contexts in existence at any one given time. These contexts will form a parent-child relationship. When a child context is saved it will push its changes to its parent's context which will then merge these changes into its own state. At the top of this parent-child hierarchy is the main context, this context upon being saved will push its changes into the persistent store.

Persistent Store Coordinator acts as an aggregator between the various different contexts to ensure the integrity of the persistent store(s). It does this by serialising read/write operations from the contexts to its store(s). There is only one coordinator per Core Data stack.

Managed Object Model is a set of entities that define each NSManagedObject subclass. An entity can be thought of as a table in a database.

Persistent Object Store is an abstraction over the actual storage of our data. Handles communication with that storage e.g. with SQLite storage, converts fetch requests into SQL statements.

Storage it's common for this to be a SQLite store but the store could also be: XML, binary and in-memory.

Unit Testing

Unit Testing is ensuring that the smallest part (unit) of testable code in your app behaves as expected in isolation (from the other parts of your app). In object-oriented programming, a unit is often a particular method, with the unit test testing one scenario (path) through that method. A unit test does this by providing a strict, written contract that the unit under test must satisfy in order to pass. If there are multiple paths through a unit i.e. an if and else branch, then more than one unit test would be required to cover each path.

Unit tests are then combined into a test suite within a test target/project, this suite can then be run to give an increased level of confidence in that the app classes are valid.

Each unit test should be executed in isolation from other unit tests to ensure that the failure of a previous test has no impact upon the next test. It is up to the unit test to ensure that any conditions (test data, user permissions, etc) that it depends on are present before the test is run and it has to tidy up after itself when it is finished. This helps to ensure that the unit test is repeatable and not dependent upon any state on the host environment. The unit test is also responsible for ensuring that the unit under test is isolated from other methods within the app and that any calls (relationship) it makes for information with other methods are mocked out. A mocked method will then return a known, preset value without performing any computation so that if a unit test fails we can have confidence that it has failed because of the code under test rather than having to hunt down the failure in its dependencies.

Each unit test should be as quick as possible to run to ensure that during development, the feedback loop between running the unit test and making code changes is as small as possible.

Building that friendship

From the above descriptions, we can see why Core Data and Unit Testing didn't instantly hit it off. Their differences centre on two issues:

Treatment of data

Unit Testing treats data as ephemeral

The main use case of Core Data is persisting data between app executions

Tolerance for delays

Unit tests should be lightning quick to execute

Core Data typically has to communicate with a SQLite file on disk which is slow (when compared with pure memory operations)

And like building any good relationship, all the changes will come from one side 😜 - Core Data.

(Ok ok, I'm sketching a little bit now so please don't take that as genuine relationship advice)

CoreDataManager

Let's build a typical Core Data stack together and unit test it as we go.

If you want to follow along, head over to my repo and download the completed project.

Let's start with the manager that will handle setting up our Core Data stack:

We could add a unit test here and assert that the same instance of CoreDataManager was always returned when shared was called however when unit testing we should only test code that we control and the logic behind creating a singleton is handled by Swift itself - so no need to create that test class yet.

Unit tests while asserting the correctness of an implementation also act as a living form of documentation so if you did want to add a unit test to assert the same instance was returned, I won't put up too much of a fight.

As our project is being developed with an iOS deployment target of iOS 11 we can use the persistent container to simplify the Core Data stack's setup.

In the above code snippet, we added a lazy property to create an instance of NSPersistentContainer. Loading a Core Data stack can be a time-consuming task especially if migration is required. To handle this we need to add a dedicated asynchronous setup method that can handle any time-consuming tasks.

While the below method doesn't actually show the migration itself, I think it's important to create and test a realistic Core Data stack and data migrations are very much a common task in iOS apps.

In the above class, we declare a property sut (subject under test) which will hold a CoreDataManager instance that we will be testing. I prefer to use sut as it makes it immediately obvious which object we are testing and which objects are collaborators/dependencies. It's important to note that the sut property is an implicitly unwrapped optional - it's purely to make our unit tests more readable by avoiding having to handle it's optional nature elsewhere and is a technique that I would not recommend using too widely in production code. The test suite's setUp method is where the CoreDataManager instance is being created and assigned to sut.

So the test method signature tells us that we are testing the setup method and that the completion closure should be triggered.

Now with this test, we are really jumping straight into the deeper end of unit testing by testing an asynchronous method but as we can see the code isn't actually that difficult to understand. The first thing we do is create an XCTestExpectation instance, it's important to note here that we are not directly creating an XCTestExpectation instance using XCTestExpectation's init method instead we are using the convenience method provided by XCTestCase. By creating it via XCTestCase we will tie both the XCTestExpectation and XCTestCase together which will allow us to use waitForExpectations and cut down on some of the boilerplate required with expectations. If you have never used expectations before, you can think of them as promising that an action will happen within a certain time frame. Sadly like actual promises, they can be broken and when they are - the test fails.

As I'm sure you have noted, test_setup_completionCalled doesn't actually contain any asserts, this is because we are using the expectation as an implicit assert.

So we've tested that the completion closure is called but we haven't actually checked that anything was set up. A successful set up should result in our persistent store being loaded so let's add a test to check that:

As we can see test_setup_persistentStoreCreated contains an assert to check that the persistentStoreCoordinator has at least one persistentStore. It's important to note that as persistentContainer is lazy loaded merely checking that it's not nil wouldn't be a valid test as calling the property would result in creating the persistentContainer.

The two unit tests that we have added are very similar and as you can see it's possible for test_setup_persistentStoreCreated to fail for two reasons:

Completion closure not triggered

Persistent store not created

The first reason is actually being tested in test_setup_completionCalled so why have I created another test that's dependent on it here? The reason is that it's actually impossible not to check this condition as it's an implicit dependency on any test that uses this method. Now the argument could be made that these two tests should be one - effectively a test_setup_stackCreated test. I opted for two tests as I felt that it improved the readability in the event that one of those tests failed by providing a higher level of granularity for that happening. You sometimes hear people saying that a unit test should only ever have one assert and that any unit test that has more than one assert is wrong. IMHO, this is foolhardy. There are very few hard and fast rules in life, just about everything is context based - in this context having two asserts (one implicit, one explicit) in test_setup_persistentStoreCreated makes sense as both asserts are checking that the same unit of functionality is correct.

Now, the more eagled eyed 👀 among you will have spotted that we are using the default storage type for our Core Data stack (NSSQLiteStoreType) in the above tests - this creates a SQLite file on the disk and as we know any I/O operation is going to be much slower than a pure in-memory operation. It would be great if we could tell the Core Data stack which storage type to use - NSSQLiteStoreType for production and NSInMemoryStoreType for testing:

As a long-term Objective-C iOS developer, I still get a thrill about being able to provide a default value to a parameter (like we do with the storeType parameter above) without having to create a whole new method. This change will allow us to use the much quicker NSInMemoryStoreType storage type in our tests while keeping a simple interface in production. However, it's not free and because we have introduced a new way of setting up our Core Data stack we need to update our existing tests to test this new path:

If we run the above tests we are able to see the difference in running times between test_setup_persistentContainerLoadedInMemory and test_setup_persistentContainerLoadedOnDisk:

As you can see on the above execution of both tests, loading the store on disk took 17 times longer than loading the store into memory - 0.001 vs 0.017 seconds.

In real terms, this speed increase isn't much on its own but once we start adding in tests that create, update and delete NSManagedObject instances dealing with an in-memory store will allow these tests to be executed faster than if we used an on-disk store.

So far, we have made great progress on producing a Core Data stack that is unit testable but a Core Data stack without a context (or two) isn't going to be very useful - let's add some:

Good news is that we have finished creating and testing our Core Data stack and I think it wasn't actually too difficult 🎉.

Introducing ColorsDataManager

There is no point in creating a Core Data stack if we don't actually use it. In the example project we populate a collectionview with instances of a subclass of NSManagedObject - Color. To help us deal with these Color objects we will be using a ColorsDataManager:

In the above class, we have a simple manager that handles creating and deleting Color instances. As a responsible member of the Core Data community, our example app treats the backgroundContext as a read-write context and the mainContext as a read-only context - this is to ensure that any time-consuming tasks don't block the main (UI) thread. You may have noticed that the above class doesn't actually contain any mention of CoreDataManager instead this class only knows about the background context. By injecting this context into the class, we are able to decouple ColorsDataManager from CoreDataManager which should allow us to more easily test ColorsDataManager 😉.

As we can see, the majority of the above class is taken up with setting up the test suite. The test itself, merely checks that the context that we pass into ColorsDataManager is the same context that is assigned to the backgroundContext property.

You may also have noticed that coreDataStack isn't a CoreDataManager instance but instead a CoreDataTestStack instance.

CoreDataTestStack is very similar to CoreDataManager but with its asynchronous setup behaviour stripped out and the storeType always set to NSInMemoryStoreType. This class allows us to more easily set up and tear down the stack between tests without having to wait on any asynchronous tasks to complete. Another difference between CoreDataTestStack and CoreDataManager is that the two contexts that are being created are not in fact standard NSManagedObjectContext instances but are actually NSManagedObjectContextSpy instances.

If you are curious as to what a spy is, Martin Fowler has produced a very insightful article on naming test objects - it's in the section titled: The Difference Between Mocks and Stubs.

NSManagedObjectContextSpy is a subclass of NSManagedObjectContext that adds special state tracking properties. System classes occupy a grey area when it comes to if you should use them in your tests or if you need to replace them with mock/stub instances. In this case, I felt that mocking out a context's functionality would be too much work and would actually be counter-productive to what the test is attempting to achieve so I'm perfectly happy to use it directly.

There is a bit more happening here than in the previous test that we saw. We create an XCTestExpectation instance that we then assign to the expectation property on the context. As we have seen above this expectation should be fulfilled when performAndWait is called. Once that expectation has been triggered, we then check that the Color instance was created and saved into our persistent store.

We first populate our persistent (in-memory) store, call the deleteColor method and then check that the correct Color has been deleted. There is one special case - because we read on the main context and delete on the background context, the color instance passed into this method may be from the main context, the above test is not covering this case so let's add another test that does:

Pretty much the same as before with the only difference being that we retrieve the color to be deleted from the main context before passing that in.

An interesting point to note when adding those three tests is that we haven't had to add any code to clear our persistent store. This is because by using an NSInMemoryStoreType store and ensuring that we create a new stack before each test - we never actually persist data. Not only does this save us time having to write the tidy up code, it also removes a whole category of bugs where leftover state from one test affects the outcome of another due to faulty/missing clean up code.

To come back to the point about in-memory stores being quicker to use than on-disk stores, we can see a typical difference in running times for the above tests below:

In-memory store

SQLite store

Best Friends Forever?

In the above code snippets, we have seen that unit testing with Core Data doesn't need to be that much more difficult than unit testing in general, with most of that added difficulty coming in the set up of the Core Data stack. And while Core Data and Unit Testing may not become BFFs (let's be honest UIKit has that position sealed down), we've seen that they can become firm friends 👫. That friendship is built on small alterations to our Core Data stack which allows its data to be more easily thrown away and the use of special subclasses to allow us to better track state.