Flexible and Easy Unit Testing of CoreData Persistence Code

Modern and high-quality iOS applications are expected to perform flawlessly. An important input to ensuring flawless, regression-resistant code is to add comprehensive unit and integration testing as part of the development process. This article steps through a methodology for building repeatable, automated database unit tests for iOS applications using CoreData as their persistence layer.

Intended Audience

This article assumes you know the basics of using CoreData in an iOS application, and have probably used it in your own work. However the focus of this article is architectural, and even if you don’t know how to code with CoreData, the concepts here should still make sense if you understand the basics of data persistence and unit testing in iOS.

Code Samples

The code and concepts in this article were developed with Xcode 10 (beta) and Swift 4.2.

This article includes code excerpts to illustrate the concepts, but rather than embed all the code for this solution within the article text, a link to an example application in my GitHub repository is provided at the end of this article.

What is CoreData?

CoreData is the default local persistence choice for iOS (and macOS) applications. Core data is fundamentally an object-relational mapping (ORM) layer over a persisted data store. While the physical storage of CoreData objects is abstracted from the developer, CoreData is almost always used with SQLite.

If you’re new to CoreData, or just need a refresher, there are many great resources out there, such as Apple’s own Core Data Programming guide, and Getting Started with Core Data Tutorial guide at RayWenderlich.com.

How CoreData Fits in an iOS Application

The following is a highly simplified diagram of how a typical application accesses CoreData. I’ll discuss each element of the architecture below.

AppDelegate. This object represents the entry point of an iOS application, and should already be familiar to all iOS developers. If you create a project with the Use CoreData option in Xcode 10, Xcode will create basic CoreData stack for you. Within the AppDelegate object, you’ll find the the following property exploded.

This property is, essentially, the hook your application uses to access data managed by CoreData.

class AppDelegate: UIResponder, UIApplicationDelegate {
   .
   .
lazy var persistentContainer: NSPersistentContainer = { ... }
   .
   .
}

An NSPersistentContainer property has within it a setting that specifies whether its data should be saved to SQLite disk files (NSSQLiteStoreType), memory (NSInMemoryStoreType) or somewhere else (NSBinaryStoreType). The latter case is uncommon, and I won’t discuss it in this article. When no setting is specified (the default), NSSQLiteStoreType is used by CoreData to configure the container.

<projectname>.xcdatamodel. When creating a project with CoreData support, Xcode will automatically create a data model file, with a root name matching the new project name and the extension xcdatamodel. The Xcode data model editor stores your evolving design in this file, and uses this metadata file to generate low-level CoreData entity model classes for you. In Xcode 10, the generated model classes will automatically be availalbe to your XCTest target (which was not the case in some older versions of Xcode, so yay!).

StorageManager. While it’s certainly possible and acceptable to access CoreData and the auto-generated entity model classes directly throughout your application, it’s quite common to encapsulate data operations in a service class. In this architecture, I’ve done this. This approach simplifies data access code for the rest of the application, and provides some degree of encapsulation just in case the the underlying database physical layer changes in the future.

As the StorageManager object is initialized (refer to the red circle numbers in the diagram called out in these bullets):

  • It uses the .xcdatamodel (1) generated model classes to perform underlying database access.
  • It will use the global persistentContainer object (2) instantiated in the AppDelegate class, which uses the deafult SQLite (3) backing for data storage.

Production App Code (e.g. ViewController). This box in the diagram represents wherever data is fetched or saved within the app. This may be code within a View Controller, View Model, or other classes you write yourself. In this architecture, all such accesses are made by calling methods of the StorageManager object, rather than interacting directly with CoreData.

SQLite DB. In the production app, StorageManager fetches and makes database changes to physical files stored in the App’s sandbox, indicated by (3) in the above diagram. These changes are not in RAM, and the database persists between runs of the program.

The main goal for this article is to create a hybrid architecture where the persistent SQLite database is used for the production app, while a volitile in-memory database is used for unit testing.

Repeatable Unit Tests vs Persistent Disk Storage

A basic requirement for unit tests is that the application state should be exactly the same at the beginning of each run of a unit test. Having a disk-based SQLite database presents a challenge to this requirement. The database files are by definition persistent, so each test run by definition affects their state and fails to guarantee each test is identical.

That said, we could simply and easily add unit tests to the project, using the existing CoreData configuration. The resulting architecture would be as follows:

In this approach both the production app and the unit test target use the same StorageManager and xcdatamodel generated model classes. This is good because the data access objects and calling methods are unchanged.

The problem, though, is that both app and test targets will use the same Container type, which is configured with the default SQLite setting (1), resulting in the unit tests using a disk-based data store (2) that won’t start in the same state for all test runs — without writing additional pre-test initialization code.

Unit Testing with a SQLite-backed container

We could deal with this challenge by reinitializing the database, perhaps in one of the following ways:

  1. Truncate all tables
  2. Delete and recreate the disk file(s) associated with the database before each unit test

Either approach may be reasonable, and should ensure the state of all disk files would be the same before every unit test. But each of these approaches requires additional code to achieve, and may need additional maintenance as the database evolves over time. If only there was an easier way — and there is!

By leveraging CoreData’s container abstraction, we have a third — and more elegant — approach that requires no physical disk file manipulation at all.

Using In Memory Persistence for Unit Tests

To give unit tests a clean, consistent environment before each test begins requires only a minor change to the existing code base. In fact, if you compare the architecture diagram below to the previous one, you’ll note that there are no additional code modules.

The coding change is to create a custom NSPersistentContainer within the Unit Test code — one which continues to use the xcdatamodel-generated CoreData model classes, but provides a PersistentContainer configured to use a volitile, in-memory persistent storage component. This is where CoreData’s abstraction between the programming model and physical storage model comes into play.

When the Unit Test is run, the custom, in-memory backed container is passed to the Storage Manager (1), which is configured for in-memory data storage.

By contrast, the production app initializes a StorageManager without passing a Container object. In this case, StorageManager uses the Container configured in AppDelegate (2), which uses the default SQLite container type.

CoreData will use SQLite or in-memory for database access automatically (3) depending on the container configuration.

NSPersistentContainer initialization in the production App

The key to making this strategy work is to initialize StorageManager differently depending on whether it’s being used from the main App target or the Unit Test target. The following are simplified versions of the initializations for each case.

When the production app target accesses the database, it always uses the persistentContainer created as a global property of AppDelegate, illustrated in the following abridged code excerpt.

Note that this initialization is very simple, and CoreData will use its default SQLite storage configuration.

Abridged AppDelegate excerpt

class AppDelegate: UIResponder, UIApplicationDelegate {
    lazy var persistentContainer: NSPersistentContainer = {
        let container = NSPersistentContainer(name: "CoreDataUnitTesting")
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            .
            .
            .
        })
        return container
    }()
}

To use this default SQLite CoreData stack, application code needs only to create a StorageManager instance and call its methods. StorageManager will use the AppDelegate.persistentContainer whenever a custom container is not provided.

Abridged ViewController excerpt

class ViewController: UIViewController {
@IBAction func saveButtonTapped(_ sender: Any) {
            let mgr = StorageManager()

            if let city = cityField.text, let country = countryField.text {
                mgr.insertPlace(city: city, country: country)
                mgr.save()
            }
        }
    }
}

NSPersistentContainer initialization in a Unit Test

When data is accessed by a unit test, the unit test target creates its own custom Container, then passes it to the StorageManager class initializer.

StorageManager doesn’t know that the persistent layer will be in-memory (and it doesn’t care). It just passes the container it’s given to CoreData, which handles the underlying details.

The following is a simplified example of the Unit Test class.

CoreDataUnitTestingTests Excerpt

class CoreDataUnitTestingTests: XCTestCase {

    // this class instantiates its own custom storage manager, using an in-memory data backing
    var customStorageManager: StorageManager?
// Using the in-memory container unit testing requires loading the xcdatamodel to be loaded from the main bundle
    var managedObjectModel: NSManagedObjectModel = {
        let managedObjectModel = NSManagedObjectModel.mergedModel(from: [Bundle.main])!
        return managedObjectModel
    }()
// The customStorageManager specifies in-memory by providing a custom NSPersistentContainer
    lazy var mockPersistantContainer: NSPersistentContainer = {
       let container = NSPersistentContainer(name: "CoreDataUnitTesting", managedObjectModel: self.managedObjectModel)
       let description = NSPersistentStoreDescription()
       description.type = NSInMemoryStoreType
       description.shouldAddStoreAsynchronously = false

        container.persistentStoreDescriptions = [description]
        container.loadPersistentStores { (description, error) in
           .
           .
           .
        }
        return container
    }()
// Before each unit test, setUp is called, which creates a fresh, empty in-memory database for the test to use
    override func setUp() {
        super.setUp()
        customStorageManager = StorageManager(container: mockPersistantContainer)
    }
// Example of how a unit test uses the customStorageManager
    func testCheckEmpty() {
        if let mgr = self.customStorageManager {
            let rows = mgr.fetchAll()
            XCTAssertEqual(rows.count, 0)
        } else {
            XCTFail()
        }
    }
}

Note the following points in the preceding code sample:

  1. A key difference is the NSPersistentContainer definition vs. the AppDelegate version. This version overrides the default SQLite storage behavior with the optional in-memory storage.
  2. Since the xcdatamodel used for testing is part of the main app bundle, it’s necessary to reference it explicitely by initializing an NSManagedObjectModel. This was not necessary in AppDelegate, since the model and container exist in the same namespace.
  3. The initialization of StorageManager includes the in-memory container, whereas in the previous ViewController code, StorageManager’s convenience initializer that takes no parameters was used to initialize the CoreData stack with the default SQLite container.

Summary

While there‘s’ always more than one way to achieve a solid testing architecture, and this isn’t the only good solution, this architectural approach has some distinct advantages:

  1. By using in-memory (rather than SQLite) for unit testing, we know for certain that there are never remnants of prior tests included in the database that we’re testing code against.
  2. Using in-memory eliminates the need to write and maintain code that clears data objects or deletes physical files before tests run. By definition, we get a fresh, new database for every run of every unit test.
  3. If we’re already using a StorageManager pattern to encapsulate CoreData calls (which is a good practice anyway), this pattern can be applied to existing projects merely by adding a convenience initializer to the StorageManager object!
  4. This approach can be achieved entirely using out-of-the-box Xcode and iOS SDK components.

Get the Code

The code for a full, runnable sample application that incorporates the above architecture is available in my GitHub account. Use this for further study of this technique, and/or as a boilerplate for your own projects.

GitHub CoreDataUnitTesting Repository

Leave a Reply

Your email address will not be published. Required fields are marked *