Edited at

SwiftでNSManagedObjectのSubclassを使った上でテストも書きたい!書けます!

More than 3 years have passed since last update.


SwiftでNSManagedObjectのSubclassを使う

SwiftでもやっぱりCoreDataは使いたいし、NSManagedObjectのSubclassも活用してゴリゴリ使いまくりたい。でもどうやるんだろう?Xcodeで普通にSwiftでsubclassを作ってもうまく動かない!

その答えはこれ。Swiftでは暗黙的な名前空間が存在するため、モジュール名の空間に入れてあげないとsubclassを見つけられない。(たぶん。説明があってるかは謎だがこれで動く)

SwiftでNSManagedObjectのSubclassを使う場合は、対応Class名にモジュール名のPrefixを付ける

が、まだ足りない。


SwiftではXcodeでCoreDataのテストが書けないの?

上記解決方法ではモジュール名を直でCoreDataのModel定義に書いてしまっているため、モジュール名の異なる空間からはやっぱり見ることができない。そう、つまりXXXTestsというように別名が付くテストコードからは見ることができないのだ!


一応どうにかテストをできる方法はある、のだが・・・

まあこの程度の自体はすでに既出であり、stackoverflowに質問が挙げられているとともに、解決方法も提示されている

Swift cannot test core data in Xcode tests?

ざっくり解説すれば、Xcode上のエディタによって生成されたファイルを読み込む際に割り込んで、"XXX.Entity"みたいになっている各subclass名を、実行するモジュールに合わせて"XXXTests.Entity"のように置き換えているようだ。なるほどこれならプロダクトでもテストでもそれぞれふさわしいclass名が使われる気がする。

しかしこれでも実は足りない。まだテストが正常に動かない場合がある。それは、Parent Entityを設定してEntityに親子関係がある場合だ。そう、Entityはsubentitiesというプロパティを持っておりそこにはこの手法でtest用のモジュール名に置き換わっていない古いEntityが残っているのだ。

この問題への解決方法は、意外にあっけなかった。残っているなら残さなければいい。subentitiesをループでぶん回して、対応する新しいEntityに全部置き換えてやった。


SwiftでNSManagedObjectのSubclassを使った上でテストも書く

NSManagedObjectModelの部分を以下のように置き換えてください("Streak"はモジュール名に各自置き換えてください)

lazy var managedObjectModel: NSManagedObjectModel = {

// The managed object model for the application. This property is not optional...
let modelURL = NSBundle.mainBundle().URLForResource("Streak", withExtension: "momd")!
let managedObjectModel = NSManagedObjectModel(contentsOfURL: modelURL)!

// Check if we are running as test or not
let environment = NSProcessInfo.processInfo().environment as [String : AnyObject]
let isTest = (environment["XCInjectBundle"] as? String)?.pathExtension == "xctest"

// Create the module name
let moduleName = (isTest) ? "StreakTests" : "Streak"

// Create a new managed object model with updated entity class names
var newEntities = [] as [NSEntityDescription]
for (_, entity) in enumerate(managedObjectModel.entities) {
let newEntity = entity.copy() as NSEntityDescription
newEntity.managedObjectClassName = "\(moduleName).\(entity.name)"
newEntities.append(newEntity)
}
// Reset subentities for all new entities
for entity:NSEntityDescription in newEntities as [NSEntityDescription] {
let parent:NSEntityDescription = entity
var newSubentities = [] as [NSEntityDescription]
for (entityName, entity) in parent.subentitiesByName {
for currentEntity in newEntities {
if entityName == currentEntity.name {
newSubentities.append(currentEntity)
}
}
}
parent.subentities = newSubentities
}

let newManagedObjectModel = NSManagedObjectModel()
newManagedObjectModel.entities = newEntities

return newManagedObjectModel
}()

追加したコードはこれ。バカみたいな三重ループ。全Entityの、全Subentityに対して、全Entityと付き合わせて合致すれば置き換え。

    // Reset subentities for all new entities

for entity:NSEntityDescription in newEntities as [NSEntityDescription] {
let parent:NSEntityDescription = entity
var newSubentities = [] as [NSEntityDescription]
for (entityName, entity) in parent.subentitiesByName {
for currentEntity in newEntities {
if entityName == currentEntity.name {
newSubentities.append(currentEntity)
}
}
}
parent.subentities = newSubentities
}

実際に使う時には、テスト用のContextやNSManagedObjectModelを生成するコードを作っておいて、テスト時のみこのNSManagedObjectModelの置き換えが実行されるようにしてあります。こうすれば

    // Check if we are running as test or not

let environment = NSProcessInfo.processInfo().environment as [String : AnyObject]
let isTest = (environment["XCInjectBundle"] as? String)?.pathExtension == "xctest"

// Create the module name
let moduleName = (isTest) ? "StreakTests" : "Streak"

のような部分を省いて実行することができます。(test決め打ちになるので)

やはりテストを書きながらの実装は安心感が違います。皆さんもぜひともSwiftを恐れずガンガンテストを書いて快適に開発していきましょう。