Setting up a Core Data store for unit tests

Published by donnywals on

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 by using an in-memory Core Data store. This type of store is ideal to use in unit tests.

Let's see how you can configure an NSPersistentContainer to use an in memory store:

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

  let description = NSPersistentStoreDescription()
  description.type = NSInMemoryStoreType
  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.type = NSInMemoryStoreType
container.persistentStoreDescriptions = [description]

These three lines of code create a persistent store description that informs the persistent container that it should use an in-memory store rather than the default SQLite storage mechanism.

In total, there are four different storage types that you can use:

  • NSSQLiteStoreType: the default storage type, used to store data in an SQLite database
  • NSInMemoryStoreType: used to store data in memory, very useful for unit tests
  • NSXMLStoreType: used to store data in an XML file that's loaded into memory entirely when a store of this type is loaded. This store type is only available on macOS
  • NSBinaryStoreType: an atomic data store that uses a binary file as its storage. This type is rarely used in production apps since it requires that the entire object graph is loaded into memory at once. Its performance is usually really good though, so it can be useful in a limited set of scenarios.

On iOS, you will normally only use NSSQLiteStoreType for your application, and NSInMemoryStoreType for your unit tests.

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 an 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.type = NSInMemoryStoreType
      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.


Practical Combine

Learn everything you need to know about Combine and how you can use it in your projects with my new book Practical Combine. You'll get thirteen chapters, a Playground and a handful of sample projects to help you get up and running with Combine as soon as possible.

The book is available as a digital download for just $24.99!

Get Practical Combine

Receive weekly updates about my posts