I am using core-data to add/delete 'Items' (NSManagedObject) to the model.
However, I am receiving the error in the title:
Code:
Failed to call designated initializer on NSManagedObject class 'Item'
I would really appreciate it if you could tell me where I am going wrong. I presume that the problem is to do with initialising Item.
RootViewController.m
- (void)AddNew {
CDManager *manager = [[CDManager alloc] initWithManagedObjectContext:[self managedObjectContext] andDelegate:self];
[manager addNewObject:[Item itemWithDescription:#"testing" dateSet:[NSDate date] fullfillBy:[NSDate date]]];
[manager release];
}
CDManager.m
- (id) initWithManagedObjectContext:(NSManagedObjectContext *)context andDelegate:(id<CDManagerDelegateProtocol>)delegate {
if ((self = [super init])) {
[self setDelegate:delegate];
[self setContext:context];
[self setItems:[[NSMutableArray alloc] init]];
[self updateItems];
}
return self;
}
- (void)addNewObject:(Item *)item {
NSManagedObjectContext *context = _context;
Item *items = [NSEntityDescription
insertNewObjectForEntityForName:#"Item"
inManagedObjectContext:_context];
[items setDateSet:[item dateSet]];
[items setDateToFullfill:[item dateToFullfill]];
[items setItemDescription:[item itemDescription]];
NSError *error;
if (![context save:&error]) {
NSLog(#"Couldn't save due to : %#", [error localizedDescription]);
}
[_delegate manager:self didAddNewItem:items];
[self update];
}
Item.m
static Item *shared = nil;
#implementation Item
......
+ (Item *)itemWithDescription:(NSString *)d dateSet:(NSDate *)date fullfillBy:(NSDate *)dates {
#synchronized(shared) {
if (!shared || shared == NULL) {
shared = [[Item alloc] init];
}
[shared setItemDescription:d];
[shared setDateSet:date];
[shared setDateToFullfill:dates];
return shared;
}
}
The Item class is defined as a singleton. There is no provision for singleton NSManagedObject subclasses. The context won't understand how to handle it.
This code makes very little sense. Here you initialize a singleton Item object:
[manager addNewObject:[Item itemWithDescription:#"testing" dateSet:[NSDate date] fullfillBy:[NSDate date]]]
... but you don't attach it to a context before passing it to the addNewObject: method. In turn that method creates another instance of Item and then populates it with the values of the presumed singleton. Why? If your singleton code actually works, every time you create an Item instance, you will get the same object back. What is the point of creating yet another reference to the singleton and setting its own values to itself. If the singleton code doesn't work, why use a singleton in the first place?
Uses like this is why singletons have such a bad rep. Don't use singletons unless you have a lot of experience with them and they are absolutely needed. There is nothing in this code that suggests that you do need a singleton and Core Data definitely does not like them.
Maybe you do something like
Item * items = [[Item alloc]initWithEntity:[NSEntityDescription entityForName:#"Item" inManagedObjectContext:context]insertIntoManagedObjectContext:context];
Edit -
because you aren't calling a good init method of NSManaged object in your custom init method.
you could also clean that up to that to take a Context then call initWithEntity... on super there.
Related
I've got 2 classes, MPRequest and MPModel.
The MPModel class has a method to lookup something from the core data store, and if not found, creates an MPRequest to retrieve it via a standard HTTP request (The method in MPModel is static and not and instance method).
What I want is to be able to get a progress of the current HTTP request. I know how to do this, but I'm getting a little stuck on how to inform the view controller. I tried creating a protocol, defining a delegate property in the MPRequest class, altering the method in MPModel to accept this delegate, and in turn passing it to the MPRequest when it is created.
This is fine, however ARC is then releasing this delegate whilst the request is running and thus doesn't do what I want. I'm trying to avoid making my delegate object a strong reference in case it throws up any reference cycles but I don't know any other way of doing this.
To start the request, from my view controller I'm running
[MPModel findAllWithBlock:^(NSFetchedResultsController *controller, NSError *error) {
....
} sortedBy:#"name" ascending:YES delegate:self]
Inside the findAllWithBlock method, I have
MPRequest *objRequest = [MPRequest requestWithURL:url];
objRequest.delegate = delegate;
[objRequest setRequestMethod:#"GET"];
[MPUser signRequest:objRequest];
[objRequest submit:^(MPResponse *resp, NSError *err) {
...
}
And in the MPRequest class I have the following property defined :
#property (nonatomic, weak) NSObject<MPRequestDelegate> *delegate;
Any ideas or suggestions?
As requested, here is some more code on how things are being called :
In the view controller :
[MPPlace findAllWithBlock:^(NSFetchedResultsController *controller, NSError *error) {
_placesController = controller;
[_listView reloadData];
[self addAnnotationsToMap];
[_loadingView stopAnimating];
if (_placesController.fetchedObjects.count > 0) {
// We've got our places, but if they're local copies
// only, new ones may have been added so just update
// our copy
MPSyncEngine *engine = [[MPSyncEngine alloc] initWithClass:[MPPlace class]];
engine.delegate = self;
[engine isReadyToSync:YES];
[[MPSyncManager sharedSyncManager] registerSyncEngine:engine];
[[MPSyncManager sharedSyncManager] sync];
}
} sortedBy:#"name" ascending:YES delegate:self];
Here, self is never going to be released for obvious reasons, so I don't see how this is the problem.
Above, MPPlace is a subclass of MPModel, but the implementation of the findAllWithBlock:sortedBy:ascending:delegate: is entirely in MPModel
The method within MPModel looks like this
NSManagedObjectContext *context = [[MPCoreDataManager sharedInstance] managedObjectContext];
[context performBlockAndWait:^{
__block NSError *error;
NSFetchRequest *request = [[NSFetchRequest alloc] initWithEntityName:NSStringFromClass([self class])];
[request setSortDescriptors:#[[[NSSortDescriptor alloc] initWithKey:key ascending:asc]]];
NSFetchedResultsController *controller = [[NSFetchedResultsController alloc] initWithFetchRequest:request
managedObjectContext:context
sectionNameKeyPath:nil
cacheName:nil];
[controller performFetch:&error];
if (!controller.fetchedObjects || controller.fetchedObjects.count == 0) {
// Nothing found or an error, query the server instead
NSString *url = [NSString stringWithFormat:#"%#%#", kMP_BASE_API_URL, [self baseURL]];
MPRequest *objRequest = [MPRequest requestWithURL:url];
objRequest.delegate = delegate;
[objRequest setRequestMethod:#"GET"];
[MPUser signRequest:objRequest];
[objRequest submit:^(MPResponse *resp, NSError *err) {
if (err) {
block(nil, err);
} else {
NSArray *objects = [self createListWithResponse:resp];
objects = [MPModel saveAllLocally:objects forEntityName:NSStringFromClass([self class])];
[controller performFetch:&error];
block(controller, nil);
}
}];
} else {
// Great, we found something :)
block (controller, nil);
}
}];
The delegate is simply being passed on to the MPRequest object being created. My initial concern was that the MPRequest object being created was being released by ARC (which I guess it probably is) but it didn't fix anything when I changed it. I can't make it an iVar as the method is static.
The submit method of the request looks like this :
_completionBlock = block;
_responseData = [[NSMutableData alloc] init];
[self prepareRequest];
[self prepareRequestHeaders];
_connection = [[NSURLConnection alloc] initWithRequest:_urlRequest
delegate:self];
And when the app starts downloading data, it calls :
[_responseData appendData:data];
[_delegate requestDidReceive:(float)data.length ofTotal:_contentLength];
Where _contentLength is simply a long storing the expected size of the response.
Got it working. It was partly an issue with threading, where the core data thread was ending before my request, me looking at the output from a different request entirely, and the way ARC handles memory in blocks.
Thanks for the help guys
I have a UITableView in a ViewController class. The ViewController class uses a custom dataController (specified in the AppDelegate). In the dataController class I'm fetching some JSON from the web, parsing it to an NSMutableArray, then using that data to populate the UITableView in the ViewController.
This all works great, except there is a noticeable lag when the app starts up since it takes time to get the JSON and work with it. I'd like to show an empty UITableView with an activity indicator while this data is loading. Unfortunately whenever I put the code in the dataController class into a dispatch queue, the UITableView is never populated with data (the data is loaded according to the log). All I see is a blank table.
I guess my main issue is I don't know how to set up a queue in the dataController class and then update the UI with the data in that queue but in another class.
Relevant code:
from dataController class:
- (void)initializeDefaultDataList {
NSMutableArray *dataList = [[NSMutableArray alloc] init];
self.masterDataList = dataList;
dispatch_queue_t myQueue = dispatch_queue_create("name.queue.my", NULL);
dispatch_async(myQueue, ^{
NSString *jsonString = [JSONHelper JSONpostString:#"http://webservice/getData"];
NSError *jsonError = nil;
//convert string to dictionary using NSJSONSerialization
NSDictionary *jsonResults = [NSJSONSerialization JSONObjectWithData: [jsonString dataUsingEncoding:NSUTF8StringEncoding]
options: NSJSONReadingMutableContainers
error: &jsonError];
if (jsonError) NSLog(#"[%# %#] JSON error: %#", NSStringFromClass([self class]), NSStringFromSelector(_cmd), jsonError.localizedDescription);
NSArray *dataArray = [jsonResults objectForKey:#"d"];
for (NSString *dataItem in dataArray) {
[self addDataWithItem:dataItem];
}
});
}
from AppDelegate:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
UINavigationController *navigationController = (UINavigationController *)self.window.rootViewController;
MyMasterViewController *firstViewController = (MyMasterViewController *)[[navigationController viewControllers] objectAtIndex:0];
MyDataController *aDataController = [[MyDataController alloc] init];
firstViewController.dataController = aDataController;
return YES;
}
from ViewController:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
//would this go here?
dispatch_async(dispatch_get_main_queue(), ^{
MyObject *objectAtIndex = [self.dataController objectInListAtIndex:indexPath.row];
[[cell textLabel] setText:objectAtIndex.name];
});
return cell;
}
In case you couldn't tell I'm really new to iOS and Objective C. Any help or hints you can give would be greatly appreciated. I'm not even sure if I'm expressing my question properly - it just seems that what I want to do shouldn't be this difficult. Thanks!
EDIT
Ok, so maybe this is a life cycle issue. Just realized that anything I set within the async block is nil outside the block, at least it is until it's too late to make a difference. That's why cellForRowAtIndexPath is never called - because the masterDataList being passed to the UITableView is empty. Tested this by initializing
__block NSString *s = [[NSString alloc] init];
outside the block, then setting a value inside the block:
s = #"Testing...";
and finally NSLogging the value of s after the block has supposedly run. But obviously the block hadn't run yet because s was nil.
It looks like you're doing the right thing to get back on the main thread after your work is done, but you haven't told the table view it needs to show the new data. [self.tableView reloadData] ought to help.
As I discovered in posts such as this one, data set within the async dispatch cannot be used outside the queue. As I understand it, the whole idea of GCD is that it determines when it's best to run and dispose of data.
As a result, I ended up splitting up my code so I was only using the DataController class to, well, control data (I know, revolutionary) and moved all the GCD parts to my ViewController. Amended code:
DataController class:
- (void)initializeDefaultDataList {
NSMutableArray *dataList = [[NSMutableArray alloc] init];
self.masterDataList = dataList;
}
ViewController class:
#interface ObjectMasterViewController () {
__block NSString *jsonString;
}
#end
...
- (void)getJSONString
{
jsonString = [JSONHelper JSONpostString:#"http://webservice/getData"];
}
...
- (void)initData {
NSError *jsonError = nil;
//convert string to dictionary using NSJSONSerialization
NSDictionary *jsonResults = [NSJSONSerialization JSONObjectWithData: [jsonString dataUsingEncoding:NSUTF8StringEncoding]
options: NSJSONReadingMutableContainers
error: &jsonError];
if (jsonError) NSLog(#"[%# %#] JSON error: %#", NSStringFromClass([self class]), NSStringFromSelector(_cmd), jsonError.localizedDescription);
NSArray *dataArray = [jsonResults objectForKey:#"d"];
//loop through array and add items to list
for (NSString *dataItem in dataArray) {
[self addDataWithItem:dataItem];
}
}
...
- (void)viewDidLoad
{
[super viewDidLoad];
dispatch_queue_t myQueue = dispatch_queue_create("name.queue.my", NULL);
dispatch_async(myQueue, ^{
//initalize service url string
[self getJSONString];
dispatch_async(dispatch_get_main_queue(), ^{
//retrieve data
[self initData];
//reload tableView with new data
[self.tableView reloadData];
});
});
}
Hope this can help someone who might be in the same boat I was in.
I am writing a Core Data ContextManager for a larger iOS application. A ContextManager provides an NSManagedContext that is automatically updated when other ContextManagers save their NSMangedContext to the persistent data store.
I have a unit test (TestContextManager) that creates two contexts, adds an object to one, and tests to see if the object appears in the other context. It doesn't. Why does the last test fail?
Here's the code for a ContextManager and the failing unit test. The last assert in the unit test fails. Every other assert passes. As you can see, the ContextManager relies upon getting a change notification from a different ContextManager and using mergeChangesFromContextDidSaveNotification to update itself. Notice that everything happens on the same thread for this test.
I know the NSManagedObjectContextDidSaveNotification is being sent and received correctly. I know the NSManagedObjectContextDidSaveNotification has the correct data in its userInfo dictionary.
I have also run this unit test as an application test on an actual device using an SQLite persistent store -- the same assert fails.
Thanks in advance!
ContextManager:
#import "ContextManager.h"
#implementation ContextManager
#synthesize context;
#pragma mark - Custom code
- (void)save {
NSError *error = nil;
if (self.context != nil) {
if ([self.context hasChanges] && ![self.context save:&error]) {
NSAssert1(FALSE, #"Unable to save the managed object context. UserInfo:\n%#", [error userInfo]);
}
}
return;
}
- (void)mergeChanges:(NSNotification *)notification {
if (notification.object != self.context) {
[self.context mergeChangesFromContextDidSaveNotification:notification];
}
return;
}
#pragma mark - Overridden NSObject methods
#pragma mark Creating, copying, and deallocating object
- (id)initWithPersistentStoreCoordinator:(NSPersistentStoreCoordinator *)persistentStoreCoordinator {
self = [super init];
if (self) {
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(mergeChanges:) name:NSManagedObjectContextDidSaveNotification object:nil];
self.context = [[[NSManagedObjectContext alloc] init] autorelease];
[self.context setPersistentStoreCoordinator:persistentStoreCoordinator];
}
return self;
}
- (void)dealloc {
[context release];
[super dealloc];
return;
}
#end
TestContextManager:
#import "TestContextManager.h"
#import "ContextManager.h"
#import "CoreDataManager.h"
#define TEST_MANAGED_OBJECT #"AManagedObject"
#implementation TestContextManager
- (void)testContextManager {
CoreDataManager *coreDataManager = [[CoreDataManager alloc] init];
coreDataManager.storeType = NSInMemoryStoreType;
ContextManager *contextManagerA = [coreDataManager provideContextManager];
if (!contextManagerA) STFail(#"CoreDataManager did not provide a context manager.");
NSManagedObjectContext *contextA = contextManagerA.context;
if (!contextA) STFail(#"ContextManager did not provide a managed object context.");
// setA1 has 0 objects (or whatever is initially there).
NSSet *setA1 = [contextManagerA.context registeredObjects];
[NSEntityDescription insertNewObjectForEntityForName:TEST_MANAGED_OBJECT inManagedObjectContext:contextManagerA.context];
// setA2 has 1 object.
NSSet *setA2 = [contextManagerA.context registeredObjects];
STAssertTrue([setA2 count] == [setA1 count]+1, #"Context provided by ContextManager is not accepting new objects.");
[contextManagerA save];
ContextManager *contextManagerB = [coreDataManager provideContextManager];
[NSEntityDescription insertNewObjectForEntityForName:TEST_MANAGED_OBJECT inManagedObjectContext:contextManagerB.context];
[contextManagerB save];
NSSet *setA3 = [contextManagerA.context registeredObjects];
// setA3 should have 2 objects <=== THIS TEST FAILS
STAssertTrue([setA3 count] == [setA1 count]+2, #"Context is not updating new objects.");
[coreDataManager release];
return;
}
#end
Did you actually set up the ContextManager to observe the notification for saving a managedObjectContext? You don't show that here so I just wanted to cover the simplest case.
Sorry, I should have made this a comment on Erik's post.
Thanks to littleknown for answering my question. Clearly I need to do some reading on what registeredObjects actually returns. I guess the good news here is that the actual code works -- the unit test was bad...
Here's the unit test that correctly exercises the unit under test AND passes:
#import "TestContextManager.h"
#import "ContextManager.h"
#import "CoreDataManager.h"
#define TEST_MANAGED_OBJECT #"AManagedObject"
#implementation TestContextManager
- (void)testContextManager {
CoreDataManager *coreDataManager = [[CoreDataManager alloc] init];
coreDataManager.storeType = NSInMemoryStoreType;
ContextManager *contextManagerA = [coreDataManager provideContextManager];
if (!contextManagerA) STFail(#"CoreDataManager did not provide a context manager.");
NSManagedObjectContext *contextA = contextManagerA.context;
if (!contextA) STFail(#"ContextManager did not provide a managed object context.");
NSEntityDescription *entityDescriptionA = [NSEntityDescription entityForName:TEST_MANAGED_OBJECT inManagedObjectContext:contextA];
// make A1 request on an empty context (0 objects)
NSFetchRequest *requestA1 = [[NSFetchRequest alloc] init];
[requestA1 setEntity:entityDescriptionA];
NSError *errorA1 = nil;
NSArray *arrayA1 = [contextA executeFetchRequest:requestA1 error:&errorA1];
if (arrayA1 == nil) STFail(#"Fetch request A1 failed.");
if ([arrayA1 count] != 0) STFail(#"Context A1 is not empty at start of test.");
// add an object to context A and make request A2
[NSEntityDescription insertNewObjectForEntityForName:TEST_MANAGED_OBJECT inManagedObjectContext:contextManagerA.context];
NSFetchRequest *requestA2 = [[NSFetchRequest alloc] init];
[requestA2 setEntity:entityDescriptionA];
NSError *errorA2 = nil;
NSArray *arrayA2 = [contextA executeFetchRequest:requestA2 error:&errorA2];
if (arrayA2 == nil) STFail(#"Fetch request A2 failed.");
if ([arrayA2 count] != 1) STFail(#"Context A2 did not successfully add an object.");
// add an object to context B and make request B1
ContextManager *contextManagerB = [coreDataManager provideContextManager];
NSManagedObjectContext *contextB = contextManagerB.context;
NSEntityDescription *entityDescriptionB = [NSEntityDescription entityForName:TEST_MANAGED_OBJECT inManagedObjectContext:contextB];
[NSEntityDescription insertNewObjectForEntityForName:TEST_MANAGED_OBJECT inManagedObjectContext:contextManagerB.context];
NSFetchRequest *requestB1 = [[NSFetchRequest alloc] init];
[requestB1 setEntity:entityDescriptionB];
NSError *errorB1 = nil;
NSArray *arrayB1 = [contextB executeFetchRequest:requestB1 error:&errorB1];
if (arrayB1 == nil) STFail(#"Fetch request B1 failed.");
if ([arrayB1 count] != 1) STFail(#"Context B1 did not successfully add an object.");
// save contextB
[contextManagerB save];
// check if contextA was updated
NSFetchRequest *requestA3 = [[NSFetchRequest alloc] init];
[requestA3 setEntity:entityDescriptionA];
NSError *errorA3 = nil;
NSArray *arrayA3 = [contextA executeFetchRequest:requestA3 error:&errorA3];
if (arrayA3 == nil) STFail(#"Fetch request A3 failed.");
if ([arrayA3 count] != 2) STFail(#"Context A did not update correctly.");
[requestA1 release];
[requestA2 release];
[requestB1 release];
[requestA3 release];
[coreDataManager release];
return;
}
#end
I have come across an interesting conundrum (of course, I could just being doing something horribly wrong).
I would like an NSTokenField to "represent" a relationship in a Core Data Application. The premise is such: You click on a Note from a TableView (loaded from the Notes Array Controller). The token field is then bound (through "value") to the Notes Array Controller selection.Tags. Tags is a to-many relationship on the entity Notes.
Obviously, an NSTokenField will not accept the NSSet that the Array Controller Provides it. To get around this, I subclassed NSTokenFieldCell and overrode its objectValue and setObjectValue: methods. I thought that I could simply translate the NSSet that was being provided to the NSArray that the NSTokenFieldCell expected. (Note: I originally tried overriding these methods on a NSTokenField subclass; however, they were not being called.)
So, I came up with said code:
- (void)setObjectValue:(NSSet*)object {
tagsList = [object copy];
NSMutableArray *displayList = [[NSMutableArray alloc] init];
for (id newObject in tagsList) {
[displayList addObject:[newObject valueForKey:#"Name"]];
}
[super setObjectValue:displayList];
}
- (id)objectValue {
NSArray *displayList = [super objectValue];
NSEntityDescription *tagEntity = [NSEntityDescription
entityForName:#"Tag"
inManagedObjectContext:[appDelegate
managedObjectContext]];
NSMutableSet *returnValue = [[NSMutableSet alloc] init];
for (NSString *token in displayList) {
NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease];
[request setEntity:tagEntity];
NSPredicate *predicate = [NSPredicate predicateWithFormat:
#"Name == %#", token];
[request setPredicate:predicate];
NSError *error;
NSArray *results = [[appDelegate managedObjectContext] executeFetchRequest:request error:&error];
if (results == nil) {
NSManagedObject *object = [NSEntityDescription insertNewObjectForEntityForName:#"Tag" inManagedObjectContext:[appDelegate managedObjectContext]];
[object setValue:token forKey:#"Name"];
[returnValue addObject:object];
} else {
[returnValue addObject:[results objectAtIndex:0]];
}
}
return returnValue;
}
It crashes. :( And, surprisingly it crashes on the line that calls [super objectValue]. It gives me the error:
-[NSConcreteAttributedString countByEnumeratingWithState:objects:count:]: unrecognized selector sent to instance ...
Sigh. The sad thing is that when I go into the Core Data XML file and give the Note a Tag, it displays correctly, and [super setObjectValue:] is passed an array of strings. However, as soon as I enter something else and mouse away, I get the error.
I am not sure what to do about this. Can anyone spot anything horribly wrong with this? Thanks.
UPDATE:
If it makes a difference, I do not have a delegate configured for the TokenField.
In typical SO fashion, I found the answer to my own question. It was silly to begin with. I simply needed another ArrayController bound to the Notes selection.Tags set. Then, I bound the NSTokenField to the ArrangedObjects of that Controller, implemented some delegate methods. Boom. Simple.
Silly me.
I am just grasping the concepts of TDD and mocking, and am running into an issue in terms of how to properly. I have a sheet that drops down and lets a user create a new core data object and save it to the data store. I am not sure if I am taking the best approach to testing it.
- (IBAction)add:(id)sender
{
NSString *itemName = [self.itemNameTextField stringValue];
SGItem *newItem = [NSEntityDescription insertNewObjectForEntityForName:kItemEntityName inManagedObjectContext:[self managedObjectContext]];
newItem.name = itemName;
NSError *error = nil;
BOOL canSaveNewItem = [[self managedObjectContext] save:&error];
if (!canSaveNewItem)
{
[NSApp presentError:error];
}
[self clearFormFields]; // Private method that clears text fields, disables buttons
[NSApp endSheet:[self window] returnCode:NSOKButton];
}
I'm trying to write two test methods to test this: one that tests the scenario where the managed object can't save and one where it successfully saves.
#interface SGAddItemWindowControllerTests : SGTestCase
{
#private
SGAddItemWindowController *addItemWindowController;
id mockApp;
id mockNameField;
}
- (void)setUp
{
mockNameField = [OCMockObject mockForClass:[NSTextField class]];
mockApp = [OCMockObject mockForClass:[NSApplication class]];
addItemWindowController = [[BLAddItemWindowController alloc] init];
[addItemWindowController setValue:mockNameField forKey:#"itemNameTextField"];
}
- (void)testAddingNewItemFromSheetFailed
{
// Setup
NSString *fakeName = #"";
[[[mockNameField expect] andReturn:fakeName] stringValue];
[[mockApp expect] presentError:[OCMArg any]];
// Execute
[addItemWindowController add:nil];
// Verify
[mockApp verify];
}
- (void)testAddingNewItemFromSheetSucceeds
{
// Setup
NSString *fakeName = #"Item Name";
[[[mockNameField expect] andReturn:fakeName] stringValue];
[[mockApp expect] endSheet:[OCMArg any] returnCode:NSOKButton];
// Execute
[addItemWindowController add:nil];
// Verify
[mockApp verify];
[mockNameField verify];
}
#end
Here are the issues I know I have, but am not sure how to work out:
I am not sure how to handle dealing with the managed object context in terms of the test. Should I bring up the entire core data stack or just create a mock of NSManagedObjectContext?
The idea of just setting the text field values as the way to trigger the if statement seems wrong. Ideally I think I should stub out the save: method and return YES or NO, but given question 1 I'm not sure about the Core Data aspects of it all.
I think I'm on the right track, but I could use a second opinion on how to tackle my issues and set me on the right path for testing the code snippet.
Justin,
What I do for question #1 is to create an actual NSManagedObjectContext but create an im-memory persistence store. Nothing hits the disk and I test the CoreData version of the truth.
I have a MWCoreDataTest class (extends in my case GTMTestCase) that builds the moc and initializes the persistence store
- (NSManagedObjectContext *) managedObjectContext {
if (managedObjectContext != nil) {
return managedObjectContext;
}
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil) {
managedObjectContext = [[NSManagedObjectContext alloc] init];
[managedObjectContext setPersistentStoreCoordinator: coordinator];
}
return managedObjectContext;
}
- (NSPersistentStoreCoordinator*)persistentStoreCoordinator;
{
if (persistentStoreCoordinator) return persistentStoreCoordinator;
NSError* error = nil;
NSManagedObjectModel *mom = [self managedObjectModel];
persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc]
initWithManagedObjectModel:mom];
if (![persistentStoreCoordinator addPersistentStoreWithType:NSInMemoryStoreType
configuration:nil
URL:nil
options:nil
error:&error]) {
[[NSApplication sharedApplication] presentError:error];
return nil;
}
return persistentStoreCoordinator;
}
WRT #2, I think that's ok - if you plan on testing more than one behavior in the class, move the
[addItemWindowController setValue:mockNameField forKey:#"itemNameTextField"];
to the testAdding.. method
If you solve #1, then you could just set the itemNameText field to nil and your save validation would trigger.
WRT #3, I would validate that building a mock on NSApp === building a mock on NSApplication
What is that you want to test? Do you want to test that Core Data does the saving or not? Or, do you want to test that your application responds correctly to the result of the call to CoreData?
Either way I think you should extract a method that performs the saving along the lines of:
-(BOOL)saveNewItem:(NSString *)itemName error:(NSError **)error {
SGItem *newItem = [NSEntityDescription insertNewObjectForEntityForName:kItemEntityName inManagedObjectContext:[self managedObjectContext]];
newItem.name = itemName;
NSError *error = nil;
return[[self managedObjectContext] save:&error];
}
- (IBAction)add:(id)sender {
NSString *itemName = [self.itemNameTextField stringValue];
NSError *error = nil;
BOOL canSaveNewItem = [self saveNewItem:itemName error:&error];
if (!canSaveNewItem) {
[NSApp presentError:error];
}
[self clearFormFields]; // Private method that clears text fields, disables buttons
[NSApp endSheet:[self window] returnCode:NSOKButton];
}
This way you can test that Core Data saving works as expected by settings up an in memory store and not have to care about the business logic. You should also be able to override or mock the result of this method for testing the business logic.
I would perhaps even move all the Core Data stuff to a separate class that would encapsulate the interaction for easier mocking.