Frage

I am trying to Unit test my model in Xcode 5 by using the XCTest classes and methods.

Because my model classes inherit of managedObject, I can't just instantiate (alloc/init) them and call the getters and setters or the methods I need to test. I need to create them by using the NSEntityDescription and use a managedObjectContext.

That is with this point that I get troubles. I don't know where and how to create the managedObjectContext for unit tests purpose.

If anyone has some advice or code examples, it will be very helpful. Thanks.

War es hilfreich?

Lösung

I use an in memory store to for my unit tests and create all the entities within that.

This class method can be placed in TestsHelper.m

+ (NSManagedObjectContext *)managedObjectContextForTests {
    static NSManagedObjectModel *model = nil;
    if (!model) {
        model = [NSManagedObjectModel mergedModelFromBundles:[NSBundle allBundles]];
    }

    NSPersistentStoreCoordinator *psc = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model];
    NSPersistentStore *store = [psc addPersistentStoreWithType:NSInMemoryStoreType configuration:nil URL:nil options:nil error:nil];
    NSAssert(store, @"Should have a store by now");

    NSManagedObjectContext *moc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
    moc.persistentStoreCoordinator = psc;

    return moc;
}

This works for me because I use Dependency Injection to pass my moc around rather than using a singleton.

Andere Tipps

I concur with @Abizern: pass an instance to a NSManagedObjectContext around in your code instead of relying on your app delegate, a global variable, or a custom helper singleton.

Dependency Injection

If you know that some controller will need access, add a NSManagedObjectContext parameter to its init method and keep a strong reference to it:

@interface SomeController : NSObject
@property (nonatomic, strong, readwrite) NSManagedObjectContext *context;
- (instancetype)initWithContext:(NSManagedObjectContext *)context;
@end

This is the minimum requirement to do "dependency injection". You don't need a fancy framework to do the injection for you. Instead, in your app you assign your usual NSManagedObjectContext instance which probably uses a SQLite store. In your tests, you create a separate NSManagedObjectContext with an in-memory store and pass it to SomeController. This would even work with (partial) mocks using OCMock.

There are some good examples at objc.io #4, especially Chris Eidhof's example app: http://www.objc.io/issue-4/full-core-data-application.html

How to access a MOC

Looking at the objc.io example code at GitHub, you'll see a PersistentStack helper class which takes care of initializing the managed object context for your application. Chris uses an abstract test case subclass to provide a testing context.

The overall guideline is this:

  1. Don't rely on a singleton helper class throughout the code because it's hard to replace its context with a test context. That's a peculiarity I have discovered and which is due to XCTest being "injected" into the running app code. There were working examples on the web, but they didn't do what I expected with Xcode 5.1 and XCTest.
  2. Instead, prepare a NSManagedObjectContext once where appropriate. Pass managed objects around and use the managed objects to get access to the context.

Florian Kugler states it this way:

Managed objects are supposed to be passed around in the application, crossing at least the model-controller barrier, and potentially even the controller-view barrier. The latter is somewhat more controversial though, and can be abstracted in a better way by e.g. defining a protocol to which an object must conform in order to be consumed by a certain view, or by implementing configuration methods in a view category that bridge the gap from the model object to the specifics of the view.

Anyway, we shouldn’t limit managed objects to the model layer and pull out their data into different structures as soon as we want to pass them around. Managed objects are first-class citizens in a Core Data app and we should use them accordingly. For example, managed objects should be passed between view controllers to provide them with the data they need.

In order to access the managed object context we often see code like this in view controllers:

NSManagedObjectContext *context = 
  [(MyApplicationDelegate *)[[UIApplication sharedApplication] delegate] managedObjectContext];

If you already pass a model object to the view controller, it’s much better to access the context directly via this object:

NSManagedObjectContext *context = self.myObject.managedObjectContext;

This removes the hidden dependency on the application delegate, and makes it much more readable and also easier to test.

This is the best overall advice I got. Now I'm able to test Core Data usage where I need to. Previously, accessing the context via global singleton class (custom class or app delegate) was convenient to use in production, but proves difficult to validate in tests.

Swift

NSManagedObjectContext extension that creates an in-memory CoreData stack for tests

extension NSManagedObjectContext {
    
    class func contextForTests() -> NSManagedObjectContext {
        // Get the model
        let model = NSManagedObjectModel.mergedModel(from: Bundle.allBundles)!
        
        // Create and configure the coordinator
        let coordinator = NSPersistentStoreCoordinator(managedObjectModel: model)
        try! coordinator.addPersistentStore(ofType: NSInMemoryStoreType, configurationName: nil, at: nil, options: nil)
        
        // Setup the context
        let context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
        context.persistentStoreCoordinator = coordinator
        return context
    }
    
}

Then use it in your tests:

class YourTests: XCTestCase {
    
    private var context: NSManagedObjectContext?

    override func setUp() {
        self.context = NSManagedObjectContext.contextForTests()
    }

    // And use it in your tests
}
Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top