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.

BFFs

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 a model layer framework that helps with creating an object graph of our domain/model objects. This object graph can be persisted between executions of the app, with that data typically being persisted in a SQLite file. However as we soon see this persistence is opt-in functionality. In fact as you get to know Core Data you will discover that a lot of it's functionality, such as relationships, persistence, validation, version control, undo, etc is opt-in. This flexibility has lead to Core Data gaining a reputation as being hard to use and while this is not without some validity, Apple has been working hard to simplify Core Data and make it a more user friendly framework. This flexibility has also meant that it's not uncommon for each project to have a slightly different Core Data setup. In this post our Core Data setup will follow:

Core-Data-Stack-Parent-Child

  • 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 as mentioned above, it's common for this to be a SQLite file but it can also take the form of 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
  • A 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:

class CoreDataManager {

    // MARK: - Singleton

    static let shared = CoreDataManager()
}

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.

lazy var persistentContainer: NSPersistentContainer! = {
    let persistentContainer = NSPersistentContainer(name: "TestingWithCoreData_Example")

    return persistentContainer
}()

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 depending on if a data 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)

// MARK: - SetUp

 func setup(completion: (() -> Void)?) {
     loadPersistentStore {
         completion?()
     }
 }

 // MARK: - Loading

 private func loadPersistentStore(completion: @escaping () -> Void) {
     //handle data migration on a different thread/queue here
     persistentContainer.loadPersistentStores { description, error in
         guard error == nil else {
             fatalError("was unable to load store \(error!)")
         }

         completion()
     }
 }

Ok so now we have something to test - does calling setup actually set up our stack. Let's create that unit test class:

class CoreDataManagerTests: XCTestCase {

    // MARK: Properties

    var sut: CoreDataManager!

    // MARK: - Lifecycle

    override func setUp() {
        super.setUp()

        sut = CoreDataManager()
    }

    // MARK: - Tests

    // MARK: Setup

    func test_setup_completionCalled() {
        let setupExpectation = expectation(description: "set up completion called")

        sut.setup() {
            setupExpectation.fulfill()
        }

        waitForExpectations(timeout: 1.0, handler: nil)
    }
}

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.

Let's take a closer look at the unit test itself:

func test_setup_completionCalled() {
    let setupExpectation = expectation(description: "set up completion called")

    sut.setup() {
        setupExpectation.fulfill()
    }

    waitForExpectations(timeout: 1.0, handler: nil)
}

When it comes to naming I follow the naming convention:

test_[unit under test]_[condition]_[expected outcome]

([condition] is optional)

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 boiler plate 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:

func test_setup_persistentStoreCreated() {
   let setupExpectation = expectation(description: "set up completion called")

    sut.setup() {
        setupExpectation.fulfill()
    }

    waitForExpectations(timeout: 1.0) { (_) in
        XCTAssertTrue(self.sut.persistentContainer.persistentStoreCoordinator.persistentStores.count > 0)
    }
}

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 in fact 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:

  1. Completion closure not triggered
  2. 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 in fact 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:

class CoreDataManager {

    private var storeType: String!

    lazy var persistentContainer: NSPersistentContainer! = {
        let persistentContainer = NSPersistentContainer(name: "TestingWithCoreData_Example")
        let description = persistentContainer.persistentStoreDescriptions.first
        description?.type = storeType

        return persistentContainer
    }()

    // MARK: - Singleton

    static let shared = CoreDataManager()

    // MARK: - SetUp

    func setup(storeType: String = NSSQLiteStoreType, completion: (() -> Void)?) {
        self.storeType = storeType

        loadPersistentStore {
            completion?()
        }
    }

    // MARK: - Loading

    private func loadPersistentStore(completion: @escaping () -> Void) {
        persistentContainer.loadPersistentStores { description, error in
            guard error == nil else {
                fatalError("was unable to load store \(error!)")
            }

            completion()
        }
    }
}

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:

class CoreDataManagerTests: XCTestCase {

    // MARK: Properties

    var sut: CoreDataManager!

    // MARK: - Lifecycle

    override func setUp() {
        super.setUp()

        sut = CoreDataManager()
    }

    // MARK: - Tests

    // MARK: Setup

    func test_setup_completionCalled() {
        let setupExpectation = expectation(description: "set up completion called")

        sut.setup(storeType: NSInMemoryStoreType) {
            setupExpectation.fulfill()
        }

        waitForExpectations(timeout: 1.0, handler: nil)
    }

    func test_setup_persistentStoreCreated() {
       let setupExpectation = expectation(description: "set up completion called")

        sut.setup(storeType: NSInMemoryStoreType) {
            setupExpectation.fulfill()
        }

        waitForExpectations(timeout: 1.0) { (_) in
            XCTAssertTrue(self.sut.persistentContainer.persistentStoreCoordinator.persistentStores.count > 0)
        }
    }

    func test_setup_persistentContainerLoadedOnDisk() {
        let setupExpectation = expectation(description: "set up completion called")
        
        sut.setup {
            XCTAssertEqual(self.sut.persistentContainer.persistentStoreDescriptions.first?.type, NSSQLiteStoreType)
            setupExpectation.fulfill()
        }
        
        waitForExpectations(timeout: 1.0) { (_) in
            self.sut.persistentContainer.destroyPersistentStore()
        }
    }

    func test_setup_persistentContainerLoadedInMemory() {
        let setupExpectation = expectation(description: "set up completion called")

        sut.setup(storeType: NSInMemoryStoreType) {
            XCTAssertEqual(self.sut.persistentContainer.persistentStoreDescriptions.first?.type, NSInMemoryStoreType)
            setupExpectation.fulfill()
        }

        waitForExpectations(timeout: 1.0, handler: nil)
    }
}

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

Persistent Store Loading Times

As you can see on tha 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 - lets add some:

lazy var backgroundContext: NSManagedObjectContext = {
    let context = self.persistentContainer.newBackgroundContext()
    context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy

    return context
}()

lazy var mainContext: NSManagedObjectContext = {
    let context = self.persistentContainer.viewContext
    context.automaticallyMergesChangesFromParent = true

    return context
}()

And we need to add some unit tests for them:

func test_backgroundContext_concurrencyType() {
    let setupExpectation = expectation(description: "background context")

    sut.setup(storeType: NSInMemoryStoreType) {
        XCTAssertEqual(self.sut.backgroundContext.concurrencyType, .privateQueueConcurrencyType)
        setupExpectation.fulfill()
    }

    waitForExpectations(timeout: 1.0, handler: nil)
}

func test_mainContext_concurrencyType() {
    let setupExpectation = expectation(description: "main context")

    sut.setup(storeType: NSInMemoryStoreType) {
        XCTAssertEqual(self.sut.mainContext.concurrencyType, .mainQueueConcurrencyType)
        setupExpectation.fulfill()
    }

    waitForExpectations(timeout: 1.0, handler: nil)
}

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

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:

class ColorsDataManager {

    let backgroundContext: NSManagedObjectContext

    // MARK: - Init

    init(backgroundContext: NSManagedObjectContext = CoreDataManager.shared.backgroundContext) {
        self.backgroundContext = backgroundContext
    }

    // MARK: - Create

    func createColor() {
        backgroundContext.performAndWait {
            let color = NSEntityDescription.insertNewObject(forEntityName: Color.className, into: backgroundContext) as! Color
            color.hex = UIColor.random.hexString
            color.dateCreated = Date()

            try? backgroundContext.save()
        }
    }
    
    // MARK: - Deletion
    
    func deleteColor(color: Color) {
        let objectID = color.objectID
        backgroundContext.performAndWait {
            if let colorInContext = try? backgroundContext.existingObject(with: objectID) {
                backgroundContext.delete(colorInContext)
                
                try? backgroundContext.save()
            }
        }
    }
}

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 😉.

Let's look at implementing our first test for ColorsDataManager:

class ColorsDataManagerTests: XCTestCase {

    // MARK: Properties

    var sut: ColorsDataManager!

    var coreDataStack: CoreDataTestStack!

    // MARK: - Lifecycle

    override func setUp() {
        super.setUp()

        coreDataStack = CoreDataTestStack()

        sut = ColorsDataManager(backgroundContext: coreDataStack.backgroundContext)
    }

    // MARK: - Tests

    // MARK: Init

    func test_init_contexts() {
        XCTAssertEqual(sut.backgroundContext, coreDataStack.backgroundContext)
    }
}

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 the coreDataStack that we use isn't actually of type CoreDataManager but instead CoreDataTestStack.

Let's go on a slight detour and have a look at CoreDataTestStack:

class CoreDataTestStack {

    let persistentContainer: NSPersistentContainer
    let backgroundContext: NSManagedObjectContextSpy
    let mainContext: NSManagedObjectContextSpy

    init() {
        persistentContainer = NSPersistentContainer(name: "TestingWithCoreData_Example")
        let description = persistentContainer.persistentStoreDescriptions.first
        description?.type = NSInMemoryStoreType

        persistentContainer.loadPersistentStores { description, error in
            guard error == nil else {
                fatalError("was unable to load store \(error!)")
            }
        }

        mainContext = NSManagedObjectContextSpy(concurrencyType: .mainQueueConcurrencyType)
        mainContext.automaticallyMergesChangesFromParent = true
        mainContext.persistentStoreCoordinator = persistentContainer.persistentStoreCoordinator

        backgroundContext = NSManagedObjectContextSpy(concurrencyType: .privateQueueConcurrencyType)
        backgroundContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
        backgroundContext.parent = self.mainContext
    }
}

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.

class NSManagedObjectContextSpy: NSManagedObjectContext {
    var expectation: XCTestExpectation?

    var saveWasCalled = false

    // MARK: - Perform

    override func performAndWait(_ block: () -> Void) {
        super.performAndWait(block)

        expectation?.fulfill()
    }

    // MARK: - Save

    override func save() throws {
        save()

        saveWasCalled = true
    }
}

Ok, detour over. Let's get back to testing the ColorsDataManager class:

func test_createColor_colorCreated() {
    let performAndWaitExpectation = expectation(description: "background perform and wait")
    coreDataStack.backgroundContext.expectation = performAndWaitExpectation

    sut.createColor()

    waitForExpectations(timeout: 1) { (_) in
        let request = NSFetchRequest.init(entityName: Color.className)
        let colors = try! self.coreDataStack.backgroundContext.fetch(request)

        guard let color = colors.first else {
            XCTFail("color missing")
            return
        }

        XCTAssertEqual(colors.count, 1)
        XCTAssertNotNil(color.hex)
        XCTAssertEqual(color.dateCreated?.timeIntervalSinceNow ?? 0, Date().timeIntervalSinceNow, accuracy: 0.1)
        XCTAssertTrue(self.coreDataStack.backgroundContext.saveWasCalled)
    }
}

There is a bit more happening here than in the previous test that we seen. 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.

Testing the deletion of a Color follows a similar pattern:

func test_deleteColor_colorDeleted() {
    let performAndWaitExpectation = expectation(description: "background perform and wait")
    coreDataStack.backgroundContext.expectation = performAndWaitExpectation
    
    let colorA = NSEntityDescription.insertNewObject(forEntityName: Color.className, into: self.coreDataStack.backgroundContext) as! Color
    let colorB = NSEntityDescription.insertNewObject(forEntityName: Color.className, into: self.coreDataStack.backgroundContext) as! Color
    let colorC = NSEntityDescription.insertNewObject(forEntityName: Color.className, into: self.coreDataStack.backgroundContext) as! Color
    
    sut.deleteColor(color: colorB)
    
    waitForExpectations(timeout: 1) { (_) in
        let request = NSFetchRequest.init(entityName: Color.className)
        let backgroundContextColors = try! self.coreDataStack.backgroundContext.fetch(request)
        
        XCTAssertEqual(backgroundContextColors.count, 2)
        XCTAssertTrue(backgroundContextColors.contains(colorA))
        XCTAssertTrue(backgroundContextColors.contains(colorC))
        XCTAssertTrue(self.coreDataStack.backgroundContext.saveWasCalled)
    }
}

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 lets add another test that does:

func test_deleteColor_switchingContexts_colorDeleted() {
    let performAndWaitExpectation = expectation(description: "background perform and wait")
    coreDataStack.backgroundContext.expectation = performAndWaitExpectation
    
    let colorA = NSEntityDescription.insertNewObject(forEntityName: Color.className, into: self.coreDataStack.backgroundContext) as! Color
    let colorB = NSEntityDescription.insertNewObject(forEntityName: Color.className, into: self.coreDataStack.backgroundContext) as! Color
    let colorC = NSEntityDescription.insertNewObject(forEntityName: Color.className, into: self.coreDataStack.backgroundContext) as! Color
    
    let mainContextColor = coreDataStack.mainContext.object(with: colorB.objectID) as! Color
    
    sut.deleteColor(color: mainContextColor)
    
    waitForExpectations(timeout: 1) { (_) in
        let request = NSFetchRequest.init(entityName: Color.className)
        let backgroundContextColors = try! self.coreDataStack.backgroundContext.fetch(request)
        
        XCTAssertEqual(backgroundContextColors.count, 2)
        XCTAssertTrue(backgroundContextColors.contains(colorA))
        XCTAssertTrue(backgroundContextColors.contains(colorC))
        XCTAssertTrue(self.coreDataStack.backgroundContext.saveWasCalled)
    }
}

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 a 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 quickier to use than on-disk stores, we can see a typical difference in running times for the above tests below:

In-memory store

Timings of in-memory store

SQLite store

Timings with 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.

What do you think? Let me know by getting in touch on Twitter - @wibosco