Setting up a Core Data store for unit tests

Published on: July 27, 2020

Unit testing is an important skill in the toolbox of any engineer. Knowing how to write a reliable, robust test suite helps you write reliable and robust code. If you've followed my introduction to unit testing part one and part two, or if you're experienced with unit testing you know that your tests should run isolated from any other tests. You also know that you should make sure that your test relies on as few external dependencies as possible.

When you want to test your Core Data code, it might not be immediately obvious how you can test your Core Data store in isolation.

Luckily, you can configure your NSPersistentContainer to write data to memory rather than disk. There are two ways to achieve this. One option is to create a persistent store description that has its type property set to NSInMemoryStoreType. When you use this method of creating an in-memory store, you will find that certain features like cascading deletes might not work as expected. These features appear to rely on features from the default storage (SQLite) that are not available to an in-memory store.

Luckily we can use an SQLite store that writes to memory by configuring a persistent store that writes data to /dev/null/ instead of an actual SQLite file:

lazy var persistentContainer: NSPersistentContainer = {
  let container = NSPersistentContainer(name: "YourDataStore")

  let description = NSPersistentStoreDescription()
  description.url = URL(fileURLWithPath: "/dev/null")
  container.persistentStoreDescriptions = [description]

  container.loadPersistentStores(completionHandler: { _, error in
    if let error = error as NSError? {
      fatalError("Failed to load stores: \(error), \(error.userInfo)")
    }
  })

  return container
}()

Most of this code should look familiar to you. The only difference between this code and a standard Core Data setup are the following three lines:

let description = NSPersistentStoreDescription()
description.url = URL(fileURLWithPath: "/dev/null")
container.persistentStoreDescriptions = [description]

These three lines of code create a persistent store description that informs the persistent container that it should write data to /dev/null while using the default SQLite storage mechanism.

While it's not documented anywhere publicly, Apple's current recommendation appears to favor using an SQLite store that writes to /dev/null over an NSInMemoryStoreType based store. Writing to /dev/null effectively uses an in-memory store, except you get all the features that you also get from the SQLite store that your app uses. This makes unit testing with a /dev/null based store far more accurate than an NSInMemoryStoreType based store.

Note:
The initial version of this article covered NSInMemoryStoreType. Thanks to some feedback and information from Geoff Pado and Vojta Stavik I found out that writing to /dev/null is the currently preferred way to create an in-memory store. Apple talks about it in this WWDC video, and you can learn more about in-memory SQLite stores here.

Unfortunately, Apple has not updated their documentation for NSInMemoryStoreType to express their latest recommendations so using the /dev/null based approach will probably remain somewhat obscure for a while.

Exposing a Core Data stack for testing from your app

Since you don't want to add any app-related code to your test target, you need to have a way to expose your testing stack to your unit tests from within your app.

I like to create a very lightweight abstraction where I initialize and manage my Core Data stack. A very bare implementation of such an abstraction looks like this:

class CoreDataStore {
  let persistentContainer: NSPersistentContainer

  init() {
    self.persistentContainer = NSPersistentContainer(name: "YourDataStore")

    self.persistentContainer.loadPersistentStores(completionHandler: { (storeDescription, error) in
      if let error = error as NSError? {
        fatalError("Unresolved error \(error), \(error.userInfo)")
      }
    })
  }
}

The code above is pretty basic but what's nice about it, is that the initialization of my persistent container is hidden from the rest of my app. This means that I have full control over how my persistent container is initialized and that I can set it up however I want.

An abstraction like this works well when you use dependency injection in your app.

Tip:
Did you know you can use dependency injection with storyboards in iOS 13 and above? Learn more here.

When you use dependency injection you can inject a different datastore into the objects that require a data store if you want to use them in your tests.

To make the abstraction I've just shown you useful for unit testing, we need to make a couple of modifications so we can choose whether a specific instance of CoreDataStore should use a regular SQLite backed store or an in-memory store:

enum StorageType {
  case persistent, inMemory
}

class CoreDataStore {
  let persistentContainer: NSPersistentContainer

  init(_ storageType: StorageType = .persistent) {
    self.persistentContainer = NSPersistentContainer(name: "YourDataStore")

    if storageType == .inMemory {
      let description = NSPersistentStoreDescription()
      description.url = URL(fileURLWithPath: "/dev/null")
      self.persistentContainer.persistentStoreDescriptions = [description]
    }

    self.persistentContainer.loadPersistentStores(completionHandler: { (storeDescription, error) in
      if let error = error as NSError? {
        fatalError("Unresolved error \(error), \(error.userInfo)")
      }
    })
  }
}

With the code above, we can create a new CoreDataStore instance and pass inMemory to its initializer to create an instance of CoreDataStore that's useful for testing. In our application code, we can explicitly pass persistent to the initializer, or pass no arguments at all because persistent is the default store type.

Imagine that you have a view model object called MainViewModel that looks as follows:

struct MainViewModel {
  let storage: CoreDataStore

  // and of course it has a bunch of properties and method
}

You can create an instance of MainViewModel in your AppDelegate, SceneDelegate, or App struct if you're using SwiftUI 2.0 as follows:

let coreDataStore = CoreDataStore()
let viewModel = MainViewModel(storage: coreDataStore)

Note:
Every time you initialize a new CoreDataStore the persistent stores are loaded again. Make sure that you only create a single instance of your Core Data storage object to avoid loading multiple copies of your persistent store.

In your tests you can create an instance of your MainViewModel that uses a temporary in memory Core Data store as follows:

let coreDataStore = CoreDataStore(.inMemory)
let viewModel = MainViewModel(storage: coreDataStore)

The view model does not "know" whether it runs in a testing environment or an app environment. It just knows that a store exists and that this store is used to read and write data.

Every time you create a new in-memory instance of CoreDataStore, you start with a fresh database. This is perfect for unit testing and you should create a new instance of your CoreDataStore for every unit test you run to make sure it has a fresh, non-polluted database that is not modified by external actors.

In Summary

In this week's post, you saw how you can create and use an in-memory version of your Core Data database that is optimized for testing. In-memory storage is wiped as soon as the persistent container associated with the in-memory store goes out of memory, and every persistent container that's created with an in-memory store starts with a completely clean slate, making it perfect for unit tests.

You also saw how you can create a very simple and plain abstraction around your Core Data store to make it very simple to create persistent and in-memory instances of your persistent container.

If you have any questions or feedback for me, make sure to reach out to me on Twitter. I would love to hear from you.

Categories

Core Data Swift

Subscribe to my newsletter