SwiftUIとCoreDataを使用するアプリのUnitTest環境構築

SwiftUIとCoreDataを組み合わせたアプリのUnitTest環境を構築する時のメモ

構築したいUnitTest環境

  • SwiftUIを使っている
  • CoreDataを使っている
  • XCodeが作ってくれたテンプレをスタート地点とする
  • プロジェクト作成時に、UnitTest, UITestをいずれもチェックした

上記の環境で、どうやってCoreDataを使うアプリのテスト環境を構築するかのメモ

CoreDataStackクラス

デフォルトで作成されるコードは、AppDelegate に NSPersistentContainer のインスタンスを保持させて、そこから取得した NSManagedObjectContext のインスタンスを SceneDelegate の中で.environment へ設定しています。

この形だと、UnitTestする時にも、AppDelegateが必要になり、すこし厄介です。CoreData関連の情報を保持するクラスを外部に独立させて作成し、そのクラスへのリファレンスを、アプリで保持するようにすると、テストしやすくなります。

CoreDataStackクラスの定義

AppDelegate 内に定義されていた CoreData 関連のコードをまとめた CoreDataStack クラスは、このようなものになります。

class CoreDataStack {
    // MARK: - Core Data stack
    lazy var persistentContainer: NSPersistentContainer = {
        /*
         The persistent container for the application. This implementation
         creates and returns a container, having loaded the store for the
         application to it. This property is optional since there are legitimate
         error conditions that could cause the creation of the store to fail.
         */
        let container = NSPersistentContainer(name: "CodeDataModel")
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                // Replace this implementation with code to handle the error appropriately.
                // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
                
                /*
                 Typical reasons for an error here include:
                 * The parent directory does not exist, cannot be created, or disallows writing.
                 * The persistent store is not accessible, due to permissions or data protection when the device is locked.
                 * The device is out of space.
                 * The store could not be migrated to the current model version.
                 Check the error message to determine what the actual problem was.
                 */
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
        return container
    }()
    
    // MARK: - Core Data Saving support
    func saveContext () {
        let context = persistentContainer.viewContext
        if context.hasChanges {
            do {
                try context.save()
            } catch {
                // Replace this implementation with code to handle the error appropriately.
                // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
                let nserror = error as NSError
                fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
            }
        }
    }
}

CoreDataStackの保持

上記で作ったCoreDataStackのインスタンスを何処かに保持させてアプリケーションの動作中に使うことになります。アプリケーションがどのように使うかによって、どこで持つべきかが変わります。ここでは、AppDelegateに保持させることとしました。合わせて、SceneDelegate での NSManagedObjectContext の取得方法も調整する必要があります。
AppDelegateのクラスに変数として以下を追加。

var coreDataStack = CoreDataStack()

SceneDelegateは、以下のように修正

// Get the managed object context from the shared persistent container.
let context = (UIApplication.shared.delegate as! AppDelegate).coreDataStack.persistentContainer.viewContext

AppDelegate 内の変数 coreDataStack 経由でNSManagedObjectContextを取得するように変更。

UnitTest

アプリケーションでは、CoreDataへの参照は、AppDelegateで保持し、SceneDelegate実行時に使われます。
UnitTestでは、AppDelegateは必要ないので、UnitTestから直接CoreDataStackをインスタンス化してテストできます。
Setupの中で行っても良いですし、別関数にしても良いですが、以下のようなコードでNSManagedObjectContextを取得できます。

func setupTestCoreData() -> NSManagedObjectContext {
    let coreDataStack = CoreDataStack()
    return coreDataStack.persistentContainer.viewContext
}

テストの改善

上記のコードから取得されるNSManagedObjectContextを使ってテストすることもできますが、CoreDataはデータをアプリ外に保持するために、過去に実行したデータが残っているかもしれないことを意識してテストする必要があります。つまり、アプリを動作させる時も、UnitTestも同じ環境になってしまうということです・・・・
ということで、テスト用のCoreDataStackを作ることを考えます。
自分で作るのも手ですが、ここにあるものを使わせてもらうのも手です。(他にも同様のライブラリはあります。)
ポイントは、SQLiteやXMLファイルをベースにするのではなく、全てをオンメモリで動作するように設定したCoreDataをテスト用に用意することです。もちろん、扱うManagedObjectModelは同じでなければいけません。
上記のライブラリを使うとそのようなことが簡単にできます。
上記のライブラリを使うとすると、テストのセットアップは以下のようになります。

MEMO
上で紹介したライブラリは、CoreDataに対しては、すごいことをしてくれるわけではなく、Bundleの.momファイルからManagedObjectModelを読んで、・・・ということをしてくれるライブラリです。勉強がてら自分で書いてみるのもアリですし、他のお気に入りのライブラリを使ってもOKです。他のライブラリを使う時には、StoreTypeを自分で指定できるAPIが用意されているかを確認してください。
func setupTestCoreData() -> NSManagedObjectContext {
  let coreDataStack = CoreDataStack(modelName: "CodeDataModel", bundleId: Bundle.main.bundleIdentifier!, storeType: NSInMemoryStoreType)
    return coreDataStack.viewContext
}

これで、シミュレータ等の環境上のCoreDataのDBファイルを汚さずに、メモリ上のDBを対象としてテストすることができるようになります。

# 上記のライブラリを使うと、CoreDataStackをインスタンスかするだけではディスク上のデータを読まないので、以下のように明示的にloadする必要があります。

class AppDelegate: UIResponder, UIApplicationDelegate {
    var coreDataStack = CoreDataStack(modelName: "CodeDataModel", bundleId: Bundle.main.bundleIdentifier!, storeType: NSSQLiteStoreType)
    func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
        self.coreDataStack.loadPersistentStores(completionHandler: {_,_ in })
        return true
    }
...

あとは、UnitTestにテストコードを追加するだけ

あとは、必要なEntityを作成して、テストするだけです。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です