Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to unit-test NSFetchedResultsController in Swift

I have a Swift app that uses NSFetchedResultsController to fetch List objects from persistent store:

let fetchedResultsController: NSFetchedResultsController = ...
var error : NSError?
fetchedResultsController.performFetch(&error)
if let error = error {
    NSLog("Error: \(error)")
}
let lists: [List] = fetchedResultsController.fetchedObjects! as [List]
NSLog("lists count = \(lists.count)")
for list: List in lists {
    NSLog("List: \(list.description)")
}

and it works like expected, I am getting List objects descriptions printed out to the console. I would like to write some unit tests for my app, so I created class that extends XCTestCase. The code compiles without a problem, tests runs, but unfortunately I am not able to fetch the List objects in that context.

All I am getting in the console is count of List objects and a fatal error:

lists count = 59
fatal error: NSArray element failed to match the Swift Array Element type

rised by the line:

for list: List in lists {

I am pretty sure I have targets configured properly, as I can create List object and insert it into managed object context without a problem from my app's source code as well as from unit test source code. The only problem I am experiencing is with fetching from test unit. I wonder why fetching is working when running the app in the simulator and fails when executed during unit test.

Any ideas what could be wrong will be appreciated.

Update:

To be more specific how my implementation looks like, here is complete code sample that I am playing with:

var error: NSError? = nil

let urls = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)
let applicationDocumentsDirectory = urls[urls.count-1] as NSURL

let modelURL = NSBundle.mainBundle().URLForResource("CheckLists", withExtension: "momd")!
let managedObjectModel = NSManagedObjectModel(contentsOfURL: modelURL)

var coordinator: NSPersistentStoreCoordinator? = NSPersistentStoreCoordinator(managedObjectModel: managedObjectModel)
let url = applicationDocumentsDirectory.URLByAppendingPathComponent("CheckLists.sqlite")
if coordinator!.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: url, options: nil, error: &error) == nil {
    NSLog("Error1: \(error)")
    abort()
}

var managedObjectContext = NSManagedObjectContext()
managedObjectContext.persistentStoreCoordinator = coordinator

let fetchRequest = NSFetchRequest()
fetchRequest.entity = NSEntityDescription.entityForName("List", inManagedObjectContext: managedObjectContext)
fetchRequest.sortDescriptors = [ NSSortDescriptor(key: "name", ascending: true) ]

let fetchedResultsController = NSFetchedResultsController(
    fetchRequest: fetchRequest,
    managedObjectContext: managedObjectContext,
    sectionNameKeyPath: nil,
    cacheName: "ListFetchedResultsControllerCache"
)

fetchedResultsController.performFetch(&error)
if let error = error {
    NSLog("Error2: \(error)")
    abort()
}

let fetchedObjects: [AnyObject]? = fetchedResultsController.fetchedObjects
if let fetchedObjects = fetchedObjects {
    NSLog("Fetched objects count: \(fetchedObjects.count)")
    for fetchedObject in fetchedObjects {
        NSLog("Fetched object: \(fetchedObject.description)")
    }
}
else {
    NSLog("Fetched objects array is nil")
}

let fetchedLists: [List]? = fetchedResultsController.fetchedObjects as? [List]
if let fetchedLists = fetchedLists {
    NSLog("Fetched lists count: \(fetchedLists.count)")
    for fetchedList in fetchedLists {
        NSLog("Fetched list: \(fetchedList.description)")
    }
}
else {
    NSLog("Fetched lists array is nil")
}

When I execute it from my app's source code, running the app in simulator, the console output looks like this:

Fetched objects count: 3
Fetched object: <CheckLists.List: 0x7a6866f0> (entity: List; id: 0x7a686020 <x-coredata://7A87B5BE-C2FA-4150-B9E3-879FDE07F0B9/List/p2> ; data: {
    name = "List 1";
})
Fetched object: <CheckLists.List: 0x7a686930> (entity: List; id: 0x7a686030 <x-coredata://7A87B5BE-C2FA-4150-B9E3-879FDE07F0B9/List/p1> ; data: {
    name = "List 2";
})
Fetched object: <CheckLists.List: 0x7a686970> (entity: List; id: 0x7a686040 <x-coredata://7A87B5BE-C2FA-4150-B9E3-879FDE07F0B9/List/p3> ; data: {
    name = "List 3";
})
Fetched lists count: 3
Fetched list: <CheckLists.List: 0x7a6866f0> (entity: List; id: 0x7a686020 <x-coredata://7A87B5BE-C2FA-4150-B9E3-879FDE07F0B9/List/p2> ; data: {
    name = "List 1";
})
Fetched list: <CheckLists.List: 0x7a686930> (entity: List; id: 0x7a686030 <x-coredata://7A87B5BE-C2FA-4150-B9E3-879FDE07F0B9/List/p1> ; data: {
    name = "List 2";
})
Fetched list: <CheckLists.List: 0x7a686970> (entity: List; id: 0x7a686040 <x-coredata://7A87B5BE-C2FA-4150-B9E3-879FDE07F0B9/List/p3> ; data: {
    name = "List 3";
})

However, when I execute this code from a unit-test, I am getting this output:

Fetched objects count: 3
Fetched object: <CheckLists.List: 0x7a07df50> (entity: List; id: 0x7a07d7e0 <x-coredata://7A87B5BE-C2FA-4150-B9E3-879FDE07F0B9/List/p2> ; data: {
    name = "List 1";
})
Fetched object: <CheckLists.List: 0x7a07e190> (entity: List; id: 0x7a07d7f0 <x-coredata://7A87B5BE-C2FA-4150-B9E3-879FDE07F0B9/List/p1> ; data: {
    name = "List 2";
})
Fetched object: <CheckLists.List: 0x7a07e1d0> (entity: List; id: 0x7a07d800 <x-coredata://7A87B5BE-C2FA-4150-B9E3-879FDE07F0B9/List/p3> ; data: {
    name = "List 3";
})
Fetched lists array is nil

I hope it makes easier to understand where the problem is. Somehow, this statement:

let fetchedLists: [List]? = fetchedResultsController.fetchedObjects as? [List]

produces an array of List objects when app is run in the simulator, but it fails producing nil when executed from unit test.

like image 856
Darrarski Avatar asked Sep 12 '14 19:09

Darrarski


1 Answers

The issue is connected with targets configuration. I have solved the problem with a little workaround.

Previously, in order to make List entity class accessible in my unit test target, I have added it to this target. So, the List class was in two targets. In fact, there were two List classes known by Swift, one for each target: MyAppTarget.List and MyUnitTestTarget.List. Retched results controller returns array of MyAppTarget.List objects, but in unit test target, List was assumed to be MyUnitTestTarget.List class. That's way this line of code:

let fetchedLists: [List]? = fetchedResultsController.fetchedObjects as? [List]

produced nil when executed from unit test target, and not the proper array as when executed from main target. To fix that I just changed it to:

let fetchedLists: [MyAppTarget.List]? = fetchedResultsController.fetchedObjects as? [MyAppTarget.List]

and make the List class public. After that change, it works like expected.

But still, it is a little bit confusing for me that MyAppTarget.List cannot be casted to MyUnitTestTarget.List. Moreover, it means that I need to make public every entity NSManagedObject subclass in order to use it inside unit tests. So far I didn't find better solution.

Perhaps there is a better way to solve that issue. I don't see an option to tell NSFetchedResultsController that it should return MyAppTarget.List in main target, and MyUnitTestTarget.List in unit test target. It will always use configuration from the .xcdatamodeld file for given entity. Also, even if there is a way to cast MyAppTarget.List into MyUnitTestTarget.List inside a unit test, it will still require the List class to be public.

Update:

I have found a way for changing class of entities returned by NSFetchedResultsController in runtime. It's a more clear and simple solution: https://stackoverflow.com/a/25858758/514181

It allows to seamlessly use CoreData entities in unit tests, without casting or making entity class public.

like image 92
Darrarski Avatar answered Nov 15 '22 05:11

Darrarski