Core Data mergeChangesFromContextDidSaveNotification Doesn't Work - objective-c

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

Related

CoreData and UITableView: display values in cells

I'm working with Core Data and web service, I want to add my data to my table,
but I don't know how should I call them, would you please help me, since when I used this way it's not working.
Here is my method for update database in my HTTP class
- (void)updateLocalCardsDataBase:(NSArray*) cardsArray
{
//check if current user has cards in local database
NSManagedObjectContext* managedObjectContext = [(AppDelegate*) [[UIApplication sharedApplication] delegate] managedObjectContext];
for(NSDictionary *cardDic in cardsArray)
{
Card *card = [NSEntityDescription insertNewObjectForEntityForName:#"Card" inManagedObjectContext:managedObjectContext];
card.remote_id = [NSNumber numberWithInt:[[cardDic objectForKey:#"id"] intValue]];
card.stampNumber = [NSNumber numberWithInt:[[cardDic objectForKey:#"stampNumber"] intValue]];
card.createdAt = [NSDate dateWithTimeIntervalSince1970:[[cardDic objectForKey:#"createdAt"] intValue]];
[managedObjectContext lock];
NSError *error;
if (![managedObjectContext save:&error])
{
NSLog(#"Whoops, couldn't save: %#", [error localizedDescription]);
NSLog(#"Failed to save to data store: %#", [error localizedDescription]);
NSArray* detailedErrors = [[error userInfo] objectForKey:NSDetailedErrorsKey];
if(detailedErrors != nil && [detailedErrors count] > 0) {
for(NSError* detailedError in detailedErrors) {
NSLog(#" DetailedError: %#", [detailedError userInfo]);
}
}
else {
NSLog(#" %#", [error userInfo]);
}
}
[managedObjectContext unlock];
}
Here is my table:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath
{
// NSManagedObjectContext* managedObjectContext = [(AppDelegate*) [[UIApplication sharedApplication] delegate] managedObjectContext];
static NSString *CellIdentifier = #"CardsCell";
CardCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil){
NSArray *objects = [[NSBundle mainBundle] loadNibNamed:#"CardCell" owner:nil options:nil];
for (id currentObject in objects)
{
if([currentObject isKindOfClass:[UITableViewCell class]])
{
cell = (CardCell *) currentObject;
break;
}
}
NSDictionary *f = [_cards objectAtIndex:indexPath.row];
cell.stampId.text = [f objectForKey:#"stampNumber"];
NSLog(#"%#fdssfdfddavds",[f objectForKey:#"stampNumber"]);
cell.createdAt.text = [f objectForKey:#"createdAt"];
cell.CardId.text = [f objectForKey:#"id"];
return cell;
}
Edit:
My problem is how I can show data in a UITableView
Before call [tableView reloadData], you need to get a data source first. You will get back an array of your data models, not an NSDictionary. You can place the my example method (or a variation that suits you best) where ever best suits your needs, but this one will not filter or sort the models, it will only get all of them. Also, I will place the method in your view controller that stores the table view:
-(NSArray*)getMycards {
NSManagedObjectContext *context = [(AppDelegate*) [[UIApplication sharedApplication] delegate] managedObjectContext];
NSEntityDescription *entityDescription = [NSEntityDescription entityForName:#"Card" inManagedObjectContext:context];
NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease];
NSError *error;
[request setEntity:entityDescription];
NSArray *cards = [context executeFetchRequest:request error:&error];
// now check if there is an error and handle it appropriatelty
// I usually return 'nil' but you don't have if you don't want
if ( error != nil ) {
// investigate error
}
return cards;
}
I recommend creating a property #property NSArray *cards in the view controller where you place your table, it will be easier to manage. One assumption I have made (since I have no other information about your view controller, a property named 'tableView' is declared in your view controller's header file (#property UITableView *tableView;), adjust the naming as needed.
With the above method, when you want to populate your array before loading the table's data:
// you put this block of code anywhere in the view controller that also has your table view
// likely in 'viewDidLoad' or 'viewDidAppear'
// and/or anywhere else where it makes sense to reload the table
self.cards = [self getMyCards];
if ( self.cards.count > 0 )
[self.tableview reloadData];
else {
// maybe display an error
}
Now, your cellForRowAtIndexPath should look like
-(UITableViewCell*tableView:tableView cellForRowAtIndexPath {
UITbaleViewCell *cell = ...;
// creating the type of cell seems fine to me
.
.
.
// keep in mind I don't know the exact make up of your card model
// I don't know what the data types are, so you will have to adjust as necessary
Card *card = self.cards[indexPath.row];
cell.stampId.text = [[NSString alloc] initWithFormat:#"%#",card.stamp];
cell.createdAt.text = [[NSString alloc] initWithFormat:#"%#",card.createdAt];
// you might want format the date property better, this might end being a lot more than what you want
cell.CardId.text = [[NSString alloc] initWithFormat:#"%#",card.id];
return cell;
}
Core Data is extremely powerful, I highly recommend the Core Data overview, followed by the Core Data Programming Guide.

store result from wcf service in core data

I am using a WCF service in my app.When the app is run for the first time on the iPad,I want it to call a WCF service and display the result in a UITableView.Alongwith displaying the data in UITableView,i want to store the data in Core Data so when the user is "offline"(not connected to wifi)the data will be displayed from the Core Data.The AppDelegate.m looks like this:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
if (![defaults objectForKey:#"firstRun"])
{
self.firstRun = TRUE;
[defaults setObject:[NSDate date] forKey:#"firstRun"];
}
else
{
self.firstRun = FALSE;//flag does exist so this ISNT the first run
}
[[NSUserDefaults standardUserDefaults] synchronize];
}
The code in UITableView looks like this:
- (void)viewDidLoad
{
[super viewDidLoad];
[my_table setDataSource:self];
[my_table setDelegate:self];
AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
if (appDelegate.firstRun){
NSLog(#"IS FIRST RUN");
EDViPadDocSyncService *service = [[EDViPadDocSyncService alloc]init];
[service getAllCategories:self action:#selector(handleGetAllCategories:)];
}
else
{
NSLog(#"NOT FIRST RUN");
NSManagedObjectContext *managedObjectContext = [self managedObjectContext];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription
entityForName:#"Categories" inManagedObjectContext:managedObjectContext];
[fetchRequest setEntity:entity];
NSError *errormsg;
self.allCats = [managedObjectContext executeFetchRequest:fetchRequest error:&errormsg];
NSLog(#"allCATS=%#",self.allCats);
self.title = #"Categories";
}
}
-(void)handleGetAllCategories:(id)value
{
if([value isKindOfClass:[NSError class]])
{
NSLog(#"This is an error %#",value);
return;
}
if([value isKindOfClass:[SoapFault class]])
{
NSLog(#"this is a soap fault %#",value);
return;
}
NSMutableArray *result = (NSMutableArray*)value;
NSMutableArray *categoryList = [[NSMutableArray alloc] init];
NSMutableArray *docCount = [[NSMutableArray alloc]init];
NSMutableArray *catIdList = [[NSMutableArray alloc]init];
self.myData = [[NSMutableArray array] init];
self.myDocCount = [[NSMutableArray array]init];
self.catId = [[NSMutableArray array]init];
for (int i = 0; i < [result count]; i++)
{
EDVCategory *catObj = [[EDVCategory alloc]init];
catObj = [result objectAtIndex:i];
[categoryList addObject:[catObj categoryName]];
[docCount addObject:[NSNumber numberWithInt:[catObj docCount]]];
[catIdList addObject:[NSNumber numberWithInt:[catObj categoryId]]];
}
self.myData = categoryList;
self.myDocCount = docCount;
self.catId = catIdList;
[my_table reloadData];
/*store data in Core Data - START*/
NSManagedObjectContext *context = [self managedObjectContext];
NSManagedObject *newCategory;
for(int j=0;j<[result count];j++)
{
newCategory = [NSEntityDescription insertNewObjectForEntityForName:#"Categories" inManagedObjectContext:context];
/*HOW TO STORE DATA FOR THE "CATEGORIES" OBJECT IN CORE DATA*/
}
/*store data in Core Data - END*/
}
I am not able to figure out how to store the data received from the wcf service to the core data object directly.I know how to store it from a text box on the screen to a core data object.eg.:-
coreDataAppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
NSManagedObjectContext *context = [appDelegate managedObjectContext];
NSManagedObject *newContact;
newCat = [NSEntityDescription insertNewObjectForEntityForName:#"Categories" inManagedObjectContext:context];
[newCat setValue:name.text forKey:#"name"];
name.text = #"";
[context save:&error];
But this doesn't help in my case.Any help is appreciated.
You are mixing networking and UI code. It is a recipe for unmaintainable code.
Your UI should be looking at Core Data and only Core Data to display its data.
Separately, and asynchronously you should be requesting data from WCF and pushing it into Core Data.
Your UI does not need to care about first run vs. subsequent run. It just looks at Core Data via a NSFetchedResultsController.
Your network code is the only part that cares about new vs. update.
Update 1
how can I achieve this? When the app is running and connected to WiFi,it has to get the latest data from the WCF service.
NSURLConnection can do async requests built-in. I generally recommend writing your networking code as NSOperation subclasses and then put them into a queue.
It appears that WCF can return XML and takes standard HTTP requests. Therefore you can write NSOperation subclasses that build your request, send it to the server and wait for a reply. When the reply comes you parse the XML and insert it into Core Data. When you save the Core Data NSManagedObjectContext your NSFetchedResultsController instances will automatically fire and allow you to update your UI.
I have several code samples that perform these feats although they are written for JSON responses as opposed to XML responses. It would not be difficult to take those examples and alter them to your needs.
You can start with this stackoverflow question and its response.
To store the data into the attributes of your NSManagedObject, simply set the values using KVC:
EDVCategory *catObject = [result objectAtIndex:j];
[newCategory setValue:[catObject categoryName] forKey#"categoryName"];
[newCategory setValue:[catObject docCount] forKey#"docCount"];
[newCategory setValue:[catObject categoryID] forKey#"categoryID"];
// after the loop
[context save:&nil];

NSManagedObject fail to save it's attributes, but able to save when adding related objects

I'm developing an iOS app using Core Data. And I have a Log entity with one-to-many relationships with Audio, Photo entities, and one-to-one relationship with Status entity. The log also has text, longitude, latitude properties. I can create the log, change its properties, add status entity, these changes would display right, until I quit the App. All the changes would disappear, and I was looking at the sqlite database, all these changes were never persisted in the database. In the database, the status object will just be created, but not linked to the log object.
But if I add an audio or photo object into the log.audioSet or log.photoSet, the changes I made to log, including the changes to text or status, will suddenly be saved into the database.
So it seems the changes are only maintained in the NSManagedObjectContext, until a related one_to_many entity is added and the [[LTLogStore sharedStore] saveChanges] will suddenly start to work.
I am using a singleton to manage the NSManagedObjectContext. Any ideas?
I would post some code if it's relevant. Thanks.
UPDATE: I'm not sure these code is enough. But basically everything works, and displays, it just doesn't save to the database. I'm using the mogenerator to set the text and latitude, but since everything is in the context. I am not sure this is the code you might need.
CODE:
#interface LTLogStore : NSObject{
}
+ (LTLogStore *)sharedStore;
- (void)removeItem:(Log *)p;
- (Log *)createItem;
- (BOOL)saveChanges;
#property(nonatomic, strong) NSFetchedResultsController *resultsController;
#property(nonatomic, strong) NSManagedObjectModel *model;
#property(nonatomic, strong) NSManagedObjectContext *context;
#end
#implementation LTLogStore
#synthesize resultsController;
#synthesize context, model;
+ (LTLogStore *)sharedStore
{
static LTLogStore *sharedStore = nil;
if(!sharedStore){
sharedStore = [[super allocWithZone:nil] init];
}
return sharedStore;
}
+ (id)allocWithZone:(NSZone *)zone
{
return [self sharedStore];
}
- (id)init
{
self = [super init];
if(self) {
model = [NSManagedObjectModel mergedModelFromBundles:nil];
NSPersistentStoreCoordinator *psc =
[[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model];
// Where does the SQLite file go?
NSString *path = [self itemArchivePath];
NSURL *storeURL = [NSURL fileURLWithPath:path];
NSError *error = nil;
if (![psc addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil
URL:storeURL
options:nil
error:&error]) {
[NSException raise:#"Open failed"
format:#"Reason: %#", [error localizedDescription]];
}
// Create the managed object context
context = [[NSManagedObjectContext alloc] init];
[context setPersistentStoreCoordinator:psc];
// The managed object context can manage undo, but we don't need it
[context setUndoManager:nil];
}
return self;
}
- (NSFetchedResultsController *)resultsController {
if (resultsController !=nil) {
return resultsController;
}
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *e = [[model entitiesByName] objectForKey:#"Log"];
[request setEntity:e];
NSSortDescriptor *sd = [NSSortDescriptor
sortDescriptorWithKey:#"created_at"
ascending:NO];
[request setSortDescriptors:[NSArray arrayWithObject:sd]];
[request setReturnsObjectsAsFaults:NO];
NSFetchedResultsController *fetchedResultsController = [[NSFetchedResultsController alloc]
initWithFetchRequest:request
managedObjectContext:context
sectionNameKeyPath:nil cacheName:#"Root"];
NSError *error;
BOOL success = [fetchedResultsController performFetch:&error];
if (!success) {
//handle the error
}
return fetchedResultsController;
}
- (NSString *)itemArchivePath
{
NSArray *documentDirectories =
NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
NSUserDomainMask, YES);
// Get one and only document directory from that list
NSString *documentDirectory = [documentDirectories objectAtIndex:0];
NSString *storePath = [documentDirectory stringByAppendingPathComponent:#"store.data"];
return storePath;
}
- (BOOL)saveChanges
{
NSError *err = nil;
BOOL successful = [context save:&err];
NSLog(#"Saving changes to the database");
if (!successful) {
NSLog(#"Error saving: %#", [err localizedDescription]);
}
return successful;
}
- (void)removeItem:(Log *)l
{
[context deleteObject:l];
[self saveChanges];
}
- (Log *)createItem
{
Log *p = [NSEntityDescription insertNewObjectForEntityForName:#"Log"
inManagedObjectContext:context];
[self saveChanges];
return p;
}
#end
#interface Log : _Log {
}
//these two are some custom convenience methods for location attributes, but it does the work of setting the longitude and latitude value in the log object, but calling the [[LTLogStore sharedStore] saveChanges] still won't save it into the database.
-(CLLocation*)location;
-(void)setLocation:(CLLocation*)location;
//this all works
-(Audio*)newAudio;
-(Audio*)newAudioWithPath:(NSString*)audioPath;
//after calling this method, even the log.text changes will be saved to the database.
-(void)addAudioWithPath:(NSString*)audioPath;
-(void)removeAudio:(Audio*)audio;
#end
#import "Log.h"
#import "Audio.h"
#import "LTLogStore.h"
#implementation Log
-(CLLocation*)location{
if (!self.longitude || !self.latitude) {
return nil;
}
CLLocation *l = [[CLLocation alloc] initWithLatitude:[self.latitude doubleValue] longitude:[self.longitude doubleValue]];
return l;
}
-(void)setLocation:(CLLocation*)location{
if (location==nil) {
self.latitude = nil;
self.longitude = nil;
}
self.latitude = [NSNumber numberWithDouble: location.coordinate.latitude];
self.longitude = [NSNumber numberWithDouble:location.coordinate.longitude];
[[LTLogStore sharedStore] saveChanges];
}
-(Audio*)newAudio{
Audio *a = [Audio new];
a.log = self;
return a;
}
-(Audio*)newAudioWithPath:(NSString*)audioPath{
Audio *new = [self newAudio];
[new setKey:audioPath];
return new;
}
-(void)addAudioWithPath:(NSString*)audioPath{
Audio *new = [self newAudio];
[new setKey:audioPath];
[[LTLogStore sharedStore] saveChanges];
}
-(void)removeAudio:(Audio*)audio{
[self.audiosSet removeObject:audio];
[[[LTLogStore sharedStore] context] deleteObject:audio];
[[LTLogStore sharedStore] saveChanges];
}
#end
UPDATE:
Problem solved, see answer.
UPDATE QUESTION: Why is my overriding causing the problem? Can someone explain the cause behind the magic of Core Data or maybe KVO behind scene?
Problem solved, I overrode the willChangeValueForKey method in the Log class, which caused the problem, I thought the code is irrelevant. But it IS:
- (void)willChangeValueForKey:(NSString *)key{
//I added the following line to fix my problem
[super willChangeValueForKey:key];
//this is the original line, I want to have this
//because I want to have a isBlank property
//so I can see if the user modified the log
_isBlank = false;
//I tried to also add the following line to be safe.
//turns out this line is not needed, and it will make the problem occur again
//[super didChangeValueForKey:key];
}

How to unit test my models now that I am using Core Data?

I have been developing an iphone application using a domain model, and have put off the persistence aspect of the app until now. Core Data looks like a really good solution since I already have a well defined model but I am running into a snag with my existing unit tests.
Here is simple example of what I have now:
- (void)test_full_name_returns_correct_string {
Patient *patient = [[Patient alloc] init];
patient.firstName = #"charlie";
patient.lastName = #"chaplin";
STAssertTrue([[patient fullName] isEqualToString:#"charlie chaplin"], #"should have matched full name");
}
How can I make this work once my Patient object extends from NSManagedObject and uses #dynamic for the firstName and lastName properties?
Has anyone else run into this type of this with Core Data? Thanks.
You need to build a Core Data stack, either within each method or in -setUp and then tear it down. Using an NSInMemoryPersistentStore will keep things fast and in-memory for your unit tests. Add a #property (nonatomic,retain) NSManagedObjectContext *moc to your TestCase subclass. Then:
- (void)setUp {
NSManagedObjectModel *mom = [NSManagedObjectModel mergedModelFromBundles:[NSArray arrayWithObject:bundleContainingXCDataModel]];
NSPersistentStoreCoordinator *psc = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:mom];
STAssertTrue([psc addPersistentStoreWithType:NSInMemoryStoreType configuration:nil URL:nil options:nil error:NULL] ? YES : NO, #"Should be able to add in-memory store");
self.moc = [[NSManagedObjectContext alloc] init];
self.moc.persistentStoreCoordinator = psc;
[mom release];
[psc release];
}
- (void)tearDown {
self.moc = nil;
}
Your test method then looks like:
- (void)test_full_name_returns_correct_string {
Patient *patient = [NSEntityDescription insertNewObjectForEntityForName:#"Person" inManagedObjectContext:self.moc];
patient.firstName = #"charlie";
patient.lastName = #"chaplin";
STAssertTrue([[patient fullName] isEqualToString:#"charlie chaplin"], #"should have matched full name");
}
assuming your entity is named Person. There was a memory leak in your version of the method, by the way; patient should be -release'd in the non-Core Data version (insertNewObjectForEntityForName:managedObjectContext: returns an autoreleased instance).
I used the answer above by Barry Wark, but I had to do some modifications to make it work with the current projects Xcode 5, iOS 7.
The property stayed the same:
#interface SIDataTest : XCTestCase
#property (nonatomic, retain) NSManagedObjectContext *moc;
#end
The setup had to actually had to change first of all to not release and secondly to provide a model URL.
- (void)setUp
{
[super setUp];
NSURL *modelURL = [[NSBundle mainBundle] URLForResource:#"SimpleInvoice" withExtension:#"momd"];
NSManagedObjectModel *mom = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
NSPersistentStoreCoordinator *psc = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:mom];
XCTAssertTrue([psc addPersistentStoreWithType:NSInMemoryStoreType configuration:nil URL:nil options:nil error:NULL] ? YES : NO, #"Should be able to add in-memory store");
self.moc = [[NSManagedObjectContext alloc] init];
self.moc.persistentStoreCoordinator = psc;
}
Here is the example test case:
- (void)testCreateNew
{
Invoice *newInvoice = [NSEntityDescription insertNewObjectForEntityForName:#"Invoice" inManagedObjectContext:self.moc];
newInvoice.dueDate = [NSDate date];
NSString* title = [[NSString alloc] initWithFormat:#"Invoice %#", #112];
newInvoice.title = title;
// Save the context.
NSError *error = nil;
if (![self.moc save:&error]) {
// Replace this implementation with code to handle the error appropriately.
// abort() 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.
XCTFail(#"Error saving in \"%s\" : %#, %#", __PRETTY_FUNCTION__, error, [error userInfo]);
}
XCTAssertFalse(self.moc.hasChanges,"All the changes should be saved");
}

Testing controller method with OCMock and Core Data

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.