Speed up your tests and SwiftUI previews by creating your Core Data stack with an in-memory store.
The Core Data stack uses an on-disk SQLite store by default. That causes some extra work when running tests as you need to reset the store to a known state before each test. It’s also slow which is a pain if you’re working with SwiftUI previews.
In Memory Store
The old way of creating an in-memory store was to change the store type in the persistent store descriptor before loading the store. The default is NSSQLiteStoreType
but we can switch to NSInMemoryStoreType
:
storeDescription.type = NSInMemoryStoreType
There’s nothing I can find in the documentation but Apple showed a different way during WWDC 2018. I was only reminded of it when looking at the project templates after creating a new Xcode Core Data project. The trick is to set the URL
of the persistent store description to /dev/null
before loading the store:
storeDescription.url = URL(fileURLWithPath: "/dev/null")
This still uses an SQLite store but we keep it in memory instead of writing it to disk. As well as being faster this also gives us a clean store each time.
NSPersistentContainer
I’m using a subclass of NSPersistentContainer
to load and configure my Core Data store. That’s a convenient place to create a custom initializer that has an option to use an in memory store:
public final class CoreDataContainer: NSPersistentContainer {
public init(name: String, bundle: Bundle = .main, inMemory: Bool = false) {
guard let mom = NSManagedObjectModel.mergedModel(from: [bundle]) else {
fatalError("Failed to create mom")
}
super.init(name: name, managedObjectModel: mom)
configureDefaults(inMemory)
}
}
Then when I configure the store I change the url
of the store description:
private func configureDefaults(_ inMemory: Bool = false) {
if let storeDescription = persistentStoreDescriptions.first {
storeDescription.shouldAddStoreAsynchronously = true
if inMemory {
storeDescription.url = URL(fileURLWithPath: "/dev/null")
storeDescription.shouldAddStoreAsynchronously = false
}
}
}
Note that I default to loading my store asynchronously to allow for slow store migrations. I don’t need to worry about that with an in-memory store. Since the store is always empty I can switch back to adding the store synchronously which keeps thing simple.
Unit Testing
In my unit tests I can now create the core data container with an in-memory store:
final class CoreDataContainerTests: XCTestCase {
private var container: CoreDataContainer!
override func setUpWithError() throws {
let container = CoreDataContainer(name: "Model", inMemory: true)
container.loadPersistentStores { description, error in
XCTAssertNil(error)
}
}
override func tearDownWithError() throws {
container = nil
}
func testPerformRequest() throws {
let context = container.viewContext
...
}
}
Note: I still keep some tests that use an on-disk store to reproduce the app environment.
SwiftUI Previews
This approach also works great for SwiftUI previews. I’ve been experimenting with keeping my Core Data objects contained in a store that publishes updates with Combine:
public final class WorldStore: ObservableObject {
@Published public private(set) var countries = [Country]()
@Published public private(set) var error: Error?
I’m creating the core data container when I initialize the store so I can again include an option for an in-memory store:
private let dataContainer: CoreDataContainer
public init(inMemory: Bool = false) {
dataContainer = CoreDataContainer(name: "World", inMemory: inMemory)
dataContainer.loadPersistentStores { ... }
}
}
I add my SwiftUI preview data as development assets:
extension Country {
static var previewData = [...]
}
I follow a similar approach to create a preview version of my store that is pre-populated with sample data:
extension WorldStore {
static var preview: WorldStore = {
let store = WorldStore(inMemory: true)
store.update(Country.previewData)
return store
}()
}
I can then use this in my SwiftUI previews:
struct WorldView_Previews: PreviewProvider {
static var previews: some View {
WorldView()
.environmentObject(WorldStore.preview)
}
}