I am trying to get started with unit testing an app that uses Core Data. In the setUp method of my unit first test, I can get the path to my data model but for some reason cannot convert it to a NSURL.
My setUp method is:
- (void)setUp {
NSBundle *bundle = [NSBundle bundleWithIdentifier:#"com.testcompany.LogicTests"];
STAssertNotNil(bundle, #"Error finding bundle to create Core Data stack.");
NSString *path = [bundle pathForResource:#"DataModel" ofType:#"momd"];
STAssertNotNil(path, #"The path to the resource cannot be nil.");
NSURL *modelURL = [NSURL URLWithString:path];
STAssertNotNil(modelURL, #"The URL to the resource cannot be nil. (tried to use path:%#, modelURL is %#)", path, modelURL);
...
}
The error I'm getting is:
/Users/neall/iPhone Apps/TestApp/UnitLogicTests.m:24:0 "((modelURL) != nil)" should be true. The URL to the resource cannot be nil. (tried to use path:/Users/neall/iPhone Apps/TestApp/build/Debug-iphonesimulator/LogicTests.octest/DataModel.momd, modelURL is (null))
I've checked the filesystem and the directory /Users/neall/iPhone Apps/TestApp/build/Debug-iphonesimulator/LogicTests.octest/DataModel.momd exists.
What am I missing here?
Thanks!
Try using [NSURL fileURLWithPath:path] instead to construct the url
Double check that you are seeing a directory called DataModel.momd at /Users/neall/iPhone Apps/TestApp/build/Debug-iphonesimulator/LogicTests.octest/DataModel.momd.
If you added a xcdatamodel file by the Add New File... command in Xcode, you would only have one file and it would be DataModel.mom (no trailing d). If that's the case, changing the
NSString *path = [bundle pathForResource:#"DataModel" ofType:#"momd"];
to
NSString *path = [bundle pathForResource:#"DataModel" ofType:#"mom"];
will fix your immediate issue.
You want to use the fileURLWithPath: that Claus suggested as well.
If you want to do versioning of your model in the future and you currently have only a .mom file, select your DataModel.xcdatamodel file in XCode and go to Design -> Data Model -> Add Model Version. This will force the creation of the DataModel.momd directory with the DataModel.mom file in it. You can just delete the new version it adds into that directory and your original tests will work.
xcdatamodel should also be added to
Project -> Targets -> "unit test target" -> build phases -> compile sources
After spending several hours stacking in July 2014 this post was one of several that in part led me to the working solution.
We somehow managed to break the surprisingly fragile (and mysterious) mechanism that links the bundle that your source code lives in to the bundle that runs the unit test. Further you might have a misnamed xcdatamodel. See comments for explanations:
-(NSManagedObjectContext *) getManagedObjectContext
{
NSManagedObjectContext *moc = [[NSManagedObjectContext alloc] init];
//Replace MyClass with class that is from your data model
//really any of your classes should work
NSBundle * bundle = [NSBundle bundleForClass:[MyClass class]];
//You can uses this line to figure you what your bundle is actually named
//In my case the because my PRODUCT_NAME had spaces in it they was replaced with '-'
//(dashes) and I couldn't divine it from the info.plist and the Build Settings.
NSString * ident =[bundle bundleIdentifier];
//This will show you where your app is actually out building temporary files
//The exact location appears to change every version or to of Xcode so
//this is useful for figuring out what your model is named
NSString * bundlePath =[bundle bundlePath];
//Here replace Name_of_model_without_the_dot_xcdatamodel with the name of your
//xcdatamodel file without an extension
//Some tutorials will have you use AppName.xcdatamodel others will simply name it
//DataModel.xcdatamodel.
//In any event if bothe path and path1 return null then check the
//bundlePath by going to Finder and pressing Command-Shift-G and pasting
//bundlePath into the pop-up. Look around for a mom or momd file thats the name you want!
NSString* path = [bundle
pathForResource:#"Name_of_model_without_the_dot_xcdatamodel"
ofType:#"momd"];
//If the above 'path' and 'path1' is not then you want to use this line instead
NSString* path1 = [bundle
pathForResource:#"Name_of_model_without the_dot_xcdatamodel"
ofType:#"mom"];
//the above path lines are simply so you can trace if you have a mom or a momd file
//replace here appropriately
NSURL *modelURL = [bundle URLForResource:#"Name_of_model_without the_dot_xcdatamodel"
withExtension:#"momd"];
//the rest is boiler plate:
NSManagedObjectModel *mom = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
NSPersistentStoreCoordinator *psc =
[[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:mom];
[psc addPersistentStoreWithType:NSInMemoryStoreType
configuration:nil URL:nil options:nil error:nil];
[moc setPersistentStoreCoordinator:psc];
return moc;
}
Here is how you might use the above context:
-(void)testMyStuff
{
NSManagedObjectContext* context=[self getManagedObjectContext];
MyClass *myobj=[NSEntityDescription insertNewObjectForEntityForName:#"MyClass"
inManagedObjectContext:context];
}
One final note you may also have to add your source files and xcmodel under the "Compile Sources" of build phases. This unfortunately changes with almost every version of Xcode. For Xcode 5:
Related
Why does my NSManagedDataModel have no entities after I add another version of the DataModel?
MacBook Pro Late 2008, OS X 10.8.5, Xcode 5.0.2,
Project has two apps, one OS X, one iOS, both share a datamodel
Recently add a new dataModel Version
DataModel involves four subClasses of NSManagedObject. Each of the four Classes received two identical changes, namely
the addition of two attributes:
modified NSDate
uid NSString
I did not select the option: Use scalar properties for primitive data types when generating
the files of the four classes.
When the following four lines execute in the OS X app,
NSString *path = [[NSBundle mainBundle] pathForResource:#"SqliteFromXml" ofType:#"momd"];
NSURL *momdURL = [NSURL fileURLWithPath:path];
managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:momdURL];
NSLog(#"path=%#\nDataModel=%#\nEntities in DM=%#",path,managedObjectModel,[managedObjectModel entities]);
This is the output:
path=/Users/appleuser/SqliteFromXml/Debug/SqliteFromXml.app/Contents/Resources/SqliteFromXml.momd
DataModel=(<NSManagedObjectModel: 0x1001d1b80>) isEditable 0, entities {
}, fetch request templates {
}
Entities in DM=( )
The momd bundle is being created on each run, but is has 0 entities.
The .app bundle Contents looks this way:
https://www.dropbox.com/s/68iy8gjqbv0m0tq/Screenshot%202014-02-16%2018.03.02.png
Many Thanks, Mark
Datamodel bundle name: IHM_Recipes.xcdatamodeld (developed as part of the original iOS app)
There are two products both sharing a common datamodel:
iHungryMePlus (iOS)
SqliteFromXml (Mac)
There was a user error in my choice of param for pathForResource: for the first line of code below:
NSString *path = [[NSBundle mainBundle] pathForResource:#"IHM_Recipes" ofType:#"momd"];
NSURL *momdURL = [NSURL fileURLWithPath:path];
managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:momdURL];
I used pathForResourceParam == #"SqliteFromXml". Sorry to waste your time. Because a momd file named SqliteFromXml.momd was created, I wrongly concluded that pathForResourceParam was
used for targeting rather than searching. I see now this was very bad logic. This approach to create a MOM came from Jeff Lamarche.
i added a key called "App" to my AppName-Info.plist manually , i can get the value from it by calling this code
NSBundle *mainBundle;
mainBundle = [NSBundle mainBundle];
NSString *value = [mainBundle objectForInfoDictionaryKey:#"App"];
NSLog(#"App: %#",value);
But what i couldn't do is changing the value with any code.. is it possible ? and if yes how can it be done ?
thanks for help in advance :)
You should consider NSUserDefaults or if you want to modify a bundled .plist try this.
NSString* plistFilePath = nil;
NSFileManager* manager = [NSFileManager defaultManager];
if ((plistFilePath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:#"mySpecial/PathTo.plist"]))
{
if ([manager isWritableFileAtPath:plistFilePath])
{
NSMutableDictionary* infoDictioio = [NSMutableDictionary dictionaryWithContentsOfFile:plistFilePath];
[infoDictio setObject:#"foo object" forKey:#"fookey"];
[infoDictio writeToFile:plistFilePath atomically:NO];
[manager setAttributes:[NSDictionary dictionaryWithObject:[NSDate date] forKey:NSFileModificationDate] ofItemAtPath:[[NSBundle mainBundle] bundlePath] error:nil];
}
}
You should not modify your apps Info.plist file (or anything in your app's bundle) at runtime. This is bad practice and will also break your bundles code signature (which will result in not being able to launch the app anymore!).
If you want to store settings you should have a look at NSUserDefaults.
It's not possible.
By default the AppName-Info.plist isn't copied into the bundle in the Copy Bundle Resources phase of the build.
So if you want to have a plist which you can write to an option would be to create it at run time in the temporary files location and read/write to it there.
This is a great place to research how to do it.
Suppose, I have added one folder name "Images" in my project.How can I get the path to that folder? My main intention is to get the number of pictures in "Images" folder.
You should work a bit more on your question: it assumes a lot and requires the reader to guess.
I have added one folder name "Images" in my project
So I guess this means you added it as a folder reference
and I want to get it's path
And I guess that you want to do that at run time from your application, not at build-time from Xcode.
If so, you could do something like:
NSURL *containingURL = [[NSBundle mainBundle] resourceURL];
NSURL *imageURL = [containingURL URLByAppendingPathComponent:#"Images" isDirectory:YES];
NSFileManager *localFileManager = [[NSFileManager alloc] init];
NSArray *content = [localFileManager contentsOfDirectoryAtURL:imageURL includingPropertiesForKeys:nil options:NSDirectoryEnumerationSkipsSubdirectoryDescendants error:NULL];
[localFileManager release];
NSUInteger imageCount = [content count];
This code does not assume that all images are of the same kind.
[[[NSBundle mainBundle] pathsForResourcesOfType:#"jpg" inDirectory:#"Images"] count];
This returns the number of jpg images from the Images folder. This is the case if you added the images to your application bundle.
I've created a project with a Core Data model in it. The application looks for the model file (.momd) and runs just fine.
Unfortunately, the unit test keeps returning null:
NSURL *dataModelURL = [[NSBundle mainBundle] URLForResource:#"myDataModel" withExtension:#"momd"];
I can see the myDataModel.xdatamodeld folder and file in BOTH the main target and the unit testing target's Compile Sources directory - but that doesn't seem to be enough. What else am I missing in the unit test target?
Thanks,
-Luther
Unfortunately, a unit test target does not use the application's main bundle but it creates a special UnitTest-bundle. So if you need to use bundled resources (like a Core Data model) within your tests, you need to work around that issue.
The most simple and most flexible workaround would be using the bundleForClass: method of NSBundle within your testing code. The parameter for that method can simply be given by [self class] within your tests. That way you can reuse this code without having to adjust the bundle identifiers in multiple projects.
Example:
- (void)testBundleLocation
{
NSBundle *bundle = [NSBundle bundleForClass:[self class]];
NSURL *url = [bundle URLForResource:#"myDataModel" withExtension:#"momd"];
...
}
The answer has to do with the bundle. A unit test target doesn't use the 'main' bundle. It creates its own bundle which, in my case, defaulted to 'com.yourcompany.UnitTest' - straight out of the [Target]-info.plist.
The corrected solution then looks like this:
NSBundle *bundle = [NSBundle bundleWithIdentifier:#"com.yourcompany.UnitTests"];
NSURL *url = [bundle URLForResource:#"myDataModel" withExtension:#"momd"];
Thanks
Had a similar problem, i solved it using the OCMock framework, so i did not need to change the application code
#interface TestCase()
#property (nonatomic, strong) id bundleMock;
#end
#implementation TestCase
- (void)setUp
{
self.bundleMock = [OCMockObject mockForClass:[NSBundle class]];
[[[self.bundleMock stub] andReturn:[NSBundle bundleForClass:[self class]]] mainBundle];
[super setUp];
}
- (void)tearDown
{
[self.bundleMock stopMocking];
[super tearDown];
}
This method will get your bundle from any target. However, for each target you add, you have to manually add the plist bundle identifier to the identifiers array, because there is no way to get it programmatically. The advantage is that you can use the same code for testing or running the application.
+(NSBundle*) getBundle
{
NSBundle *bundle = nil;
// try your manually set bundles
NSArray *identifiers = [NSArray arrayWithObjects: #"com.your.application",
#"com.your.test",
nil];
for(NSString *bundleId in identifiers) {
bundle = [NSBundle bundleWithIdentifier:bundleId];
if (bundle!=nil) break;
}
// try the main bundle
if (bundle==nil) bundle = [NSBundle mainBundle];
// abort
assert(bundle!=nil && "Missing bundle. Check the Bundle identifier on
the plist of this target vs the identifiers array in this class.");
return bundle;
}
My problem was indeed the wrong Bundle! As I was trying to use a database from/within a Framework I 'simply' has to load the db from the corresponding Bundle !
Here is some code in Swift4 using MagicalRecord:
// Load the bundle
let frameworkBundle = Bundle(for: AClassFromTheFramework.self)
let managedObjectModel = NSManagedObjectModel.mergedModel(from: [frameworkBundle])
// Use the new `managedObjectModel` by default
MagicalRecord.setShouldAutoCreateManagedObjectModel(false)
NSManagedObjectModel.mr_setDefaultManagedObjectModel(managedObjectModel)
// Load the database
MagicalRecord.setupCoreDataStack(withAutoMigratingSqliteStoreNamed: "db.sqlite")
And voilĂ !
I know this is a frequently asked question, however none of the solutions that I have found seem to work for me.
This is my situation:
I have one data model for my application, and I wanted to add versioning to it. So in XCode, I did Design -> Data Model -> Add Model Version. I also updated my delegate's persistentStoreCoordinator method to look like this:
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
if (persistentStoreCoordinator != nil) {
return persistentStoreCoordinator;
}
NSURL *storeUrl = [NSURL fileURLWithPath: [[self applicationDocumentsDirectory]
stringByAppendingPathComponent: #"foo.sqlite"]];
NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption,
[NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption,
nil];
NSError *error = nil;
persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
if(![persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeUrl options:options error:&error]) {
/*Error for store creation should be handled in here*/
}
return persistentStoreCoordinator;
}
Just to make sure everything was still working, I did a clean all, build, and tested it in the simulator. Everything worked so far.
Next I chose the new version data model, set it to be the current version using XCode, and added one extra attribute to an entity. I then did a clean all, build. And now whenever I start the application it crashes with this error: 'Can't merge models with two different entities named 'foo''.
What am I doing wrong? I have tried making sure that no data model is added to the target, adding just the current version data model to the target, and both. Every time I test I make sure to clean all.
Can anyone shed some light as to why it does not work for me?
EDIT:
here is my managedObjectModel method:
- (NSManagedObjectModel *)managedObjectModel {
if (managedObjectModel != nil) {
return managedObjectModel;
}
managedObjectModel = [[NSManagedObjectModel mergedModelFromBundles:nil] retain];
return managedObjectModel;
}
I already expected this implementation of the managedObjectModel getter.
In your implementation all models within the bundle are merged into one single model. Therefore also all versions within the .momd are merged resulting in duplicate entity definitions.
Change the code to explicitly initialize the model with the apropriate model file and it should work fine.
NSString *modelPath = [[NSBundle mainBundle] pathForResource:#"datamodel" ofType:#"momd"];
NSURL *modelURL = [NSURL fileURLWithPath:modelPath];
managedObjectModel_ = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
If you're using Version Models on Core Data you always have to initialize to the version of the model that you want to use. On the Application Bundle you will find one file with extension .momd that is the Full Model. Inside of this file you will find a lot of .mom files inside, each .mom file represent one version of your model.
If you run your application and initialize with the .momd file and all versions inside, Core Data will create all versions and later we will have the "duplicated entity" error, Core Data don't known what version use. Now, the only way to fix the problem is delete the app, point your code to the correct .mom file and run again, so Core Data create only one version of the internal database.
Here's one snippet of code that do the job:
NSString *fullModelName = #"myModel.momd"; // The name of the main model.
NSString *modelVersionName = #"myModel1.0.mom"; // Only the name of the version.
NSString *bundlePath = [[NSBundle mainBundle] bundlePath];
NSString *modelPath = [NSString stringWithFormat:#"%#/%#/%#", bundlePath, fullModelName, modelVersionName];
//
NSURL *modelURL = [NSURL fileURLWithPath:modelPath];
managedObjectModel_ = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];