Thursday, January 31, 2019

Swift 3's Core Data: Using A Separate Database File For Unit Tests

I'm developing a pet project and I have to populate data when launching the app in both Simulator and real devices. I also want to keep the data inputted while I'm manually testing the app on the Simulator. Then, it's a bit difficult for unit test. I can't let the unit tests modify or delete the pre-populated data and the inputted ones. I have to input the data differently for the unit tests and delete it back when the tests are finished. If I want to count the number of records of an entity in the store, I have to give the unit-test data negative identifiers to separate them from the pre-populated and the manually inputted ones within the app. I think it's better to have a separate database file for the unit tests alone.

In Xcode 9, when creating a Single View Application, the datastore is configured by default. The persistentContainer property (an instance of NSPersistentContainer class) is initialized in AppDelegate class. The persistentContainer encapsulates the Core Data stack in the app. It handles creation of the managed object, the persistent store coordinator, and the managed object context.

The persistentContainer object has a function called defaultDirectoryURL(), which defines the location of the datastore file. I'm going to subclass NSPersistentContainer and override this function to change the datastore's file location for unit tests.

1. Creating main.swift file


Before that, we have to comment out the @UIApplicationMain attribute on the AppDelegate class.


import Foundation
import UIKit

let isRunningTest = NSClassFromString("XCTestCase") != nil
let appDelegateClass = isRunningTest ? NSStringFromClass(UnitTestAppDelegate.self) : NSStringFromClass(AppDelegate.self)

UIApplicationMain(
    CommandLine.argc,
    UnsafeMutableRawPointer(CommandLine.unsafeArgv)
        .bindMemory(
            to: UnsafeMutablePointer.self,
            capacity: Int(CommandLine.argc)),
    nil,
    appDelegateClass
)
 

When launching the app, the system looks for @UIApplicationMain attribute on a class. Xcode adds the attribute on the AppDelegate class by default. The @UIApplicationMain acts as a main function, which initializes the UIApplication object (represents the current app instance) and its delegate (responds to important events in the app's life cycle) at run time.

According to Apple's documentation, if we don't use @UIApplicationMain, we must create a main.swift file at the top level that calls the NSApplicationMain(_:_:_:_) function. The third parameter is the name of UIApplication class or subclass. In the code snippets above, it's set to nil so UIApplication is assumed. The fourth parameter is the name of AppDelegate class or subclass. If the unit test is running (if the XCTestCase class exists in the bundle or not nil), UnitTestPersistentContainer (my custom class) is assigned. Otherwise, the default AppDelegate class is assigned.

2. Creating UnitTestAppDelegate class


import Foundation
import CoreData

class UnitTestAppDelegate : AppDelegate {
    
    override var persistentContainer: NSPersistentContainer {
        get {
            let container = UnitTestPersistentContainer(name: "MyCalculator")
            container.loadPersistentStores(completionHandler: { (storeDescription, error) in
                if let error = error as NSError? {

                    fatalError("Unresolved error \(error), \(error.userInfo)")
                }
            })
            return container
        }
        set {
            super.persistentContainer = newValue
        }
    }
}
 

3. Creating UnitTestPersistentContainer class

    
import Foundation
import CoreData

class UnitTestPersistentContainer : NSPersistentContainer {
    
    open class override func defaultDirectoryURL() -> URL {
        return URL(fileURLWithPath: NSHomeDirectory() + "/Documents")
    }
}
 

Note that the datastore file couldn't be created when I specified "~/Documents" or "/var/TMP" for the fileURLWithPath parameter. I don't know why.



References
https://developer.apple.com/documentation/coredata/nspersistentcontainer



No comments:

Post a Comment