I am working on a iOS project that uses core data. I am using swift.
The Core Data stack is setup right and all seems to be fine.
I have created a class for an entity (NSManagedObject) called TestEntity.
The class looks like this:
import UIKit
import CoreData
class TestEntity: NSManagedObject {
#NSManaged var name: NSString
#NSManaged var age: NSNumber
}
So, then I try to insert a new TestEntity in code using this line of code:
let te: TestEntity = NSEntityDescription.insertNewObjectForEntityForName("TestEntity", inManagedObjectContext: ctx) as TestEntity
I then get this error:
I have seen some answers on stack overflow that say that I need to worry about the module name. So then I looked that up on the docs:
https://developer.apple.com/library/content/documentation/Swift/Conceptual/BuildingCocoaApps/WritingSwiftClassesWithObjective-CBehavior.html
Then I went in the core data entity for TestEntity and in the class field I entered myAppName.TestEntity
When I run the app this line:
let te: TestEntity = NSEntityDescription.insertNewObjectForEntityForName("TestEntity", inManagedObjectContext: ctx) as TestEntity
still gives me the same error.
What else could I be doing wrong?
EDIT:
So, I was able to make the app not crash anymore by changing the TestEntity NSManagedObject class to:
import UIKit
import CoreData
#objc(TestEntity) class TestEntity: NSManagedObject {
#NSManaged var name: NSString
#NSManaged var age: NSNumber
}
So, I added the #objc(TestEntity) in it. This works with or without adding the appName before the TestEntity class name in the core data data model inspector.
This works, but, when I run tests this line still crashes:
let te: TestEntity = NSEntityDescription.insertNewObjectForEntityForName("TestEntity", inManagedObjectContext: ctx) as TestEntity
So I found that this is an issue for other people:
How to access Core Data generated Obj-C classes in test targets?
How can we get core data to work in tests in swift. I am NOT using a bridging header in the app target and it all works great. The test target still crashes though.
How can I fix the test target so it can run core data tests?
With Xcode 7, and #testable, you should no longer need to update the managedObjectClassName or use other hacks. Here's what I did to get it working in Xcode 7.2.
Set your test target Host Application and check "Allow testing Host Applications APIs".
Make sure none of your regular classes have a Target Membership pointing to the Test target. Only classes with unit test code should be set to the Test target.
Add the #testable line to the top of all of your test classes:
import XCTest
#testable import MyApp
class MyAppTests: XCTestCase {
}
If you're still having issues you may want to try these additional tips: https://forums.developer.apple.com/message/28773#28949
I fought with this one for a while so I hope it helps someone else out.
It's because the CoreData framework is still in Objective-C. Swift uses namespaced-classes, so for CoreData to find your swift classes you have to specify the Class name with it's namespace like this:
The problem your will have is that your App does not have the same namespace as when you are running you tests.
<AppName>.<ClassName> vs <AppName>Tests.<ClassName>
EDIT: Solution for running as App and Tests
I just wrote a piece of code to solve the <AppName>.<ClassName> vs <AppName>Tests.<ClassName> issue. The solution I use at this time (Xcode 6.1) is to NOT fill the Class field in the CoreData UI (shown above), and to do it in code instead.
This code will detect if you are running as App vs Tests and use the right module name and update the managedObjectClassName.
lazy var managedObjectModel: NSManagedObjectModel = {
// The managed object model for the application. This property is not optional...
let modelURL = NSBundle.mainBundle().URLForResource("Streak", withExtension: "momd")!
let managedObjectModel = NSManagedObjectModel(contentsOfURL: modelURL)!
// Check if we are running as test or not
let environment = NSProcessInfo.processInfo().environment as [String : AnyObject]
let isTest = (environment["XCInjectBundle"] as? String)?.pathExtension == "xctest"
// Create the module name
let moduleName = (isTest) ? "StreakTests" : "Streak"
// Create a new managed object model with updated entity class names
var newEntities = [] as [NSEntityDescription]
for (_, entity) in enumerate(managedObjectModel.entities) {
let newEntity = entity.copy() as NSEntityDescription
newEntity.managedObjectClassName = "\(moduleName).\(entity.name)"
newEntities.append(newEntity)
}
let newManagedObjectModel = NSManagedObjectModel()
newManagedObjectModel.entities = newEntities
return newManagedObjectModel
}()
I think I'm getting similar results to you. I was unable to get my tests working with the line
var newDept = NSEntityDescription.insertNewObjectForEntityForName("Department", inManagedObjectContext: moc) as Department
But I could get the tests running with :
let entity = NSEntityDescription.entityForName("Department", inManagedObjectContext: moc)
let department = Department(entity: entity!, insertIntoManagedObjectContext: moc)
My Entity looks like :
#objc(Department)
class Department: NSManagedObject {
#NSManaged var department_description: String
...
}
The code example from Ludovic does not cover subentities. So when setting a parent entity in CoreData, the app crashes.
Adapted the code to take subentities into account:
private func createManagedObjectModel() {
// Get module name
var moduleName: String = "ModuleName"
let environment = NSProcessInfo.processInfo().environment as! [String : AnyObject]
let isTest = (environment["XCInjectBundle"] as? String)?.pathExtension == "xctest"
if isTest { moduleName = "ModuleNameTests" }
// Get model
let modelURL = NSBundle.mainBundle().URLForResource(self.storeName, withExtension: "momd")!
let model = NSManagedObjectModel(contentsOfURL: modelURL)!
// Create entity copies
var newEntities = [NSEntityDescription]()
for (_, entity) in enumerate(model.entities) {
let newEntity = entity.copy() as! NSEntityDescription
newEntity.managedObjectClassName = "\(moduleName).\(entity.managedObjectClassName)"
newEntities.append(newEntity)
}
// Set correct subentities
for (_, entity) in enumerate(newEntities) {
var newSubEntities = [NSEntityDescription]()
for subEntity in entity.subentities! {
for (_, entity) in enumerate(newEntities) {
if subEntity.name == entity.name {
newSubEntities.append(entity)
}
}
}
entity.subentities = newSubEntities
}
// Set model
self.managedObjectModel = NSManagedObjectModel()
self.managedObjectModel.entities = newEntities
}
I also faced similar issue when I tried to write unit test cases for a sample app (MedicationSchedulerSwift3.0) written in Swift 3.0, apart from implementing solution provided by johnford I created a category on XCTestCase to setup an NSManagedObjectContext with in-memory store by using below code:
// XCTestCase+CoreDataHelper.swift
import CoreData
import XCTest
#testable import Medication
extension XCTestCase {
func setUpInMemoryManagedObjectContext() -> NSManagedObjectContext {
let managedObjectModel = NSManagedObjectModel.mergedModel(from: [Bundle.main])!
let persistentStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: managedObjectModel)
do {
try persistentStoreCoordinator.addPersistentStore(ofType: NSInMemoryStoreType, configurationName: nil, at: nil, options: nil)
} catch {
print("Adding in-memory persistent store failed")
}
let managedObjectContext = NSManagedObjectContext(concurrencyType:.privateQueueConcurrencyType)
managedObjectContext.persistentStoreCoordinator = persistentStoreCoordinator
return managedObjectContext
}
}
And used it like this:
// NurseTests.swift
import XCTest
import CoreData
#testable import Medication
class NurseTests: XCTestCase {
var managedObjectContext: NSManagedObjectContext?
//MARK: Overriden methods
override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
if managedObjectContext == nil {
managedObjectContext = setUpInMemoryManagedObjectContext()
}
}
//MARK:- Testing functions defined in Nurse.swift
// testing : class func addNurse(withEmail email: String, password: String, inManagedObjectContext managedObjectContext: NSManagedObjectContext) -> NSError?
func testAddNurse() {
let nurseEmail = "clara#gmail.com"
let nursePassword = "clara"
let error = Nurse.addNurse(withEmail: nurseEmail, password: nursePassword, inManagedObjectContext: managedObjectContext!)
XCTAssertNil(error, "There should not be any error while adding a nurse")
}
}
In case if someone needs more examples they can look at unit test cases over here - MedicationTests
Related
I have a set of NSManagedObject subclasses which are used by the ClassToBeTested.
The ClassToBeTested operates just on a few properties of the NSManagedObject subclasses and doesn't need relationships or the whole CoreData stack.
Can I somehow use the same objects in tests by just creating them in a regular way:
let template = CoreDataClass()
template.name = randomString(length: 40) // This fails!
templates.append(template)
Currently it fails with error:
failed: caught "NSInvalidArgumentException", "-[CoreDataClass
setTemplate_name:]: unrecognized selector sent to instance
0x600000af4c40"
Although I get a different error (did not call designated initializer) when I try to do that, in either case, the answer to your question is: No, you cannot do that.
But with NSPersistentContainer nowadays, it is easy to use a singleton in-memory Core Data Stack for such testing. Include your data model in your test bundle, then put this in your test's global scope:
var sharedTestContext: NSManagedObjectContext = {
// Swift is smart enough to execute this only once.
let container = NSPersistentContainer(name: "<YourDataModelName>")
let description = NSPersistentStoreDescription()
description.type = NSInMemoryStoreType
container.persistentStoreDescriptions = [description]
container.loadPersistentStores { (description, error) in
if let error = error {
fatalError("Failed to load store for test: \(error)")
}
}
return container.newBackgroundContext()
}()
And define a special managed object initializer for testing like this:
/**
Initializes a managed object for testing
- important: This assumes your managed object subclass name is the same
as its entity name.
*/
public extension NSManagedObject {
convenience init(testContext: NSManagedObjectContext?) {
let context = testContext ?? sharedTestContext
/* The following contraption is necessary to avoid warning:
"Multiple NSEntityDescriptions claim the NSManagedObject subclass"
For explanation see:
https://stackoverflow.com/questions/51851485/multiple-nsentitydescriptions-claim-nsmanagedobject-subclass */
let name = String(describing: type(of: self))
let entity = NSEntityDescription.entity(forEntityName: name, in: context)!
self.init(entity: entity, insertInto: context)
}
}
Now you can create your test object thus:
let template = CoreDataClass(testContext: nil)
When I try to use Core Data with NSInMemoryStoreType for unit testing I always get this error:
Failed to find a unique match for an NSEntityDescription to a managed object subclass
This is my object to create the core data stack:
public enum StoreType {
case sqLite
case binary
case inMemory
.................
}
public final class CoreDataStack {
var storeType: StoreType!
public init(storeType: StoreType) {
self.storeType = storeType
}
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "Transaction")
container.loadPersistentStores(completionHandler: { (description, error) in
if let error = error {
fatalError("Unresolved error \(error), \(error.localizedDescription)")
} else {
description.type = self.storeType.type
}
})
return container
}()
public var context: NSManagedObjectContext {
return persistentContainer.viewContext
}
public func reset() {
for store in persistentContainer.persistentStoreCoordinator.persistentStores {
guard let url = store.url else { return }
try! persistentContainer.persistentStoreCoordinator.remove(store)
try! FileManager.default.removeItem(at: url)
}
}
}
And this is how I am using it inside my unit test project:
class MyTests: XCTestCase {
var context: NSManagedObjectContext!
var stack: CoreDataStack!
override func setUp() {
stack = CoreDataStack(storeType: .inMemory)
context = stack.context
}
override func tearDown() {
stack.reset()
context = nil
}
}
From what I read here which seems to be the same issue that I have, I have to cleanup everything after every test, which I (think) I am doing.
Am I not cleaning up correctly ? Is there another way to do this ?
Is the CoreDataStack class initialised in your application? For instance, in an AppDelegate class? When the unit test is run it will initialise the AppDelegate some time before the test is ran. I believe this is so that your tests can call into anything from the app in order to test it, as per the line #testable import MyApp. If you're initialising a Core Data stack via your AppDelegate and in MyTests then you will be loading the Core Data stack twice.
Just to note, having two or more NSPersistentContainer instances means two or more NSManagedObjectModel instances will be loaded into memory, which is what causes the issue. Both models are providing additional NSManagedObject subclasses at runtime. When you then try to use one of these subclasses the runtime doesn't know which to use (even though they're identical, it just sees that they have the same name). I think it'd be better if NSManagedObjectModel could handle this case, but it's currently up to the developer to ensure there's never more than one instance loaded.
I know this question is old, however, I've encountered this problem recently and didn't find an answer elsewhere.
Building on #JamesBedford 's answer, a way to setup your Core Data stack is:
Ensure you only have a single instance of CoreDataStack in your app across both the app and test targets. Don't create new instances in your test target. In your app target, you could use a singleton as James suggests. Or, if you are keeping a strong reference to your Core Data stack in the AppDelegate and initialising at launch, provide a convenience static property in your app target to access from your test target. Something like:
extension CoreDataStack
static var shared: CoreDataStack {
(UIApplication.shared.delegate as! AppDelegate).stack
}
}
Add an environment variable to your test scheme in Xcode. Go to Xcode > Edit Scheme > Test > Arguments > Environment Variables. Add a new name-value pair such as: name = "persistent_store_type", value = "in_memory". Then, at runtime, inside your CoreDataStack initialiser, you can check for this environment variable using ProcessInfo.
final class CoreDataStack {
let storeType: StoreType
init() {
if ProcessInfo.processInfo.environment["persistent_store_type"] == "in_memory" {
self.storeType = .inMemory
} else {
self.storeType = .sqlLite
}
}
}
From here your test target will now use the .inMemory persistent store type and won't create the SQLLite store. You can even add a unit test asserting so :)
I have an iOS project that is working fine on the simulator and whatnot, but cannot seem to link up nicely to the Core Data resources when I'm working from its testing bundle.
I've made the NSManagedObjectContext from memory in the set-up function of my testing class. However, when I attempt to run the program, the test functions fail, and the console has output
"An NSManagedObject of class 'Projectname.Deck' must have a valid NSEntityDescription."
Is there something I'm missing? I'd like to be able to make unit tests for my app's data structure as I develop it.
Thanks!
Edit
Relevant sections of the test class:
class ProjectNameTests: XCTestCase {
var testDeck: Deck? = nil
func setUpInMemoryManagedObjectContext() -> NSManagedObjectContext {
let managedObjectModel = NSManagedObjectModel.mergedModel(from: [Bundle.main])!
let persistentStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: managedObjectModel)
do {
try persistentStoreCoordinator.addPersistentStore(ofType: NSInMemoryStoreType, configurationName: nil, at: nil, options: nil)
} catch {
print("Adding in-memory persistent store failed")
}
let managedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
managedObjectContext.persistentStoreCoordinator = persistentStoreCoordinator
return managedObjectContext
}//setUpInMemoryManagedObjectContext
override func setUp() {
super.setUp()
self.context = setUpInMemoryManagedObjectContext()
testDeck = Deck(context: context)
testDeck!.name = "Test Deck"
}//setUp
}//ProjectNameTests
UPDATE:
After doing a fair bit of research, I was able to figure out that my issue was related to pulling the NSEntityDescription from the class itself, and not from the current context.
I added the following method to my Deck class (and will do the same for other NSManagedObject subclasses):
public static func entityDescription(context: NSManagedObjectContext)->NSEntityDescription{
return NSEntityDescription.entity(forEntityName: String(describing: self), in: context)!
}//entityDescription
I then changed the initialization call in my test case to the following:
let deckEntity: NSEntityDescription = Deck.entityDescription(context: context)
testDeck = Deck(entity: deckEntity, insertInto: context)
This way, the object is initialized with the NSEntityDescription pulled from the current NSManagedObjectContext, which makes everything happier.
Props to Swift, Core Data, and unit testing for leading me on the right track.
I need to unit test (XCTest) some of my methods that include reference to CoreData models.
The following line execute correctly :
var airport: AnyObject! = Airport.MR_createEntity()
(lldb) po airport <Airport: 0x7fcf54216940> (entity: Airport; id: 0x7fcf54216a20 <x-coredata:///Airport/t1D3D08DA-70F9-4DA0-9487-BD6047EE93692> ; data: {
open = nil;
shortName = nil;
visible = nil; })
whereas the following line triggers an EXC_BAD_ACCESS :
var airport2: Airport = Airport.MR_createEntity() as! Airport
(lldb) po airport2
error: Execution was interrupted, reason: EXC_BAD_ACCESS (code=1, address=0x0).
The process has been returned to the state before expression evaluation.
No sign of this error with my principal target. The configuration is : model objects in both targets, class prefixed by #objc(MyModel), no namespace in class' models in my xcdatamodel
Any idea what's going on here ?
Right, so I have finally gotten to the bottom of this and its not pretty. There is actually a radar for this issue as it appears to be a bug with the Swift compiler not recognizing ManagedObject casting in test targets. So add your voice to the noise
Starting off with an entity defined as follows:
#objc(Member)
class Member: NSManagedObject {
#NSManaged var name: String
}
I wrote a simple test class in which I create a MO in 3 different ways:
The first two failed:
let context = NSManagedObjectContext.MR_defaultContext()
func testMagicalRecordCreation() {
let m = Member.MR_createInContext(context) as? Member
XCTAssertNotNil(m, "Failed to create object")//fails
}
func testEntityDescriptionClassCreation() {
let m2 = NSEntityDescription.insertNewObjectForEntityForName("Member", inManagedObjectContext: context) as? Member
XCTAssertNotNil(m2, "Failed to create object")//fails
}
And then a success
func testManualMOCreation() {
let ent = NSEntityDescription.entityForName("Member", inManagedObjectContext: context)!
let m3 = Member(entity: ent, insertIntoManagedObjectContext: context)
XCTAssertNotNil(m3, "Failed to create object")
}
This means that right now you have two options. Write your tests in Objective-C; or create a utility method to insert test objects into a context using the means I showed above.
Theres a nice post about this behaviour here
I ended up using an NSManagedObjectContext extension to be used explicitly in Swift Tests:
extension NSManagedObjectContext {
func insertTestEntity<T: NSManagedObject>(entity: T.Type) -> T {
let entityDescription = NSEntityDescription.entityForName(NSStringFromClass(T.self), inManagedObjectContext: self)!
return T(entity: entityDescription, insertIntoManagedObjectContext: self)
}
}
And it could be used like this:
func testConvenienceCreation() {
let m4 = context.insertTestEntity(Member)
XCTAssertNotNil(m4, "Failed to create object")
}
More reading about this kind of approach here
There are two ways to make your Swift application classes available to the test target:
Make your application classes public – this includes every variable, constant, and function you want to test
Add your application Swift file to your Test target. This is pretty simple to do.
I have the following structure so far:
- Singleton NetworkingManager (login, logout API calls)
- Subclass NSManagedObject (with son extension)
I don't know how to structure this part of the app?
Do I need a PersistentManager/global ObjectManagedContext?
Here are my classes:
NetworkingManager (API-call)
func getContacts() {
GET(APIURL.URL_CONTACTS ,parameters: nil,
{ (operation : NSURLSessionDataTask!, response : AnyObject!) -> Void in
var contacts = [Contacts]()
contacts <<<<* response
//_sharedPersistentManager.save(contacts!)
}, { (operation : NSURLSessionDataTask!, error : NSError!) -> Void in
println("Error contacts")
})
}
Model
import Foundation
import CoreData
class Contacts: NSManagedObject, Deserializable {
#NSManaged var firstname: String
#NSManaged var lastname: String
#NSManaged var id: String
required init(data: [String: AnyObject]) {
let managedContext = (UIApplication.sharedApplication().delegate
as AppDelegate).managedObjectContext!
let entity = NSEntityDescription.entityForName("Contacts", inManagedObjectContext: managedContext)!
super.init(entity: entity, insertIntoManagedObjectContext: managedContext)
self.id <<< data["id"]
self.firstname <<< data["1"]
self.lastname <<< data["2"]
}
}
This code fails... (Bad_ACCESS)
I struggle with the basic class design.
Would be so kind of you to post some pseudo-code and I can understand the basic concept.
All Tutorials explain how to save or fetch some data, but not a real concept for a whole app...
I'm looking for a basic concept like:
- You should implement one Persistent Manager Singleton class
//PersistentManager
- global ObjectManagedContext
- Singleton
- func save (model1: Model1)
- func save (model2: Model2)
- ....
//NetworkingManager
- Singleton
- ...
EDIT:
- added some code
- added further explanation
Typically your App would have one database (but it may have more than one, depending on your requirements). If we assume you only need one then you need to create a Core Data database (persistent store) and at a minimum one NSPersistentStoreCoordinator and one ManagedObjectContext in order to create and update database records via the ManagedObjects. Just use the template app to get sample code for doing this.
I would not try using the ManagedObjects init() method for creating the object, instead use a helper function in another class to do this. This init() method will get called automatically when you create the object so you might use it to set some default values for properties.
Below is an example ManagedObject (Swift class) and an Objective-C helper function. Apologies in advance, you will need to convert this to the equivalent Swift code.
//
// DocumentCategory.swift
// Sample
//
import Foundation
import CoreData
#objc(DocumentCategoryX)
class DocumentCategoryX: NSManagedObject {
#NSManaged var image: NSData
#NSManaged var name: String
#NSManaged var sortIndex: NSNumber
#NSManaged var viewName: String
#NSManaged var documents: NSSet
}
Sample helper function to create a managed object (a Core Data database record)
- (NSManagedObject*)createNewCategory:(NSString*)entityName sortIndex:(int)sortIndex withName:(NSString *)name viewName:(NSString *)viewName icon:(UIImage*)icon;
{
NSManagedObject *newCat = [NSEntityDescription
insertNewObjectForEntityForName:entityName
inManagedObjectContext:[self managedObjectContext]];
[newCat setValue:[NSNumber numberWithInt:sortIndex] forKey:#"sortIndex"];
[newCat setValue:name forKey:#"name"];
[newCat setValue:viewName forKey:#"viewName"];
[newCat setValue:UIImagePNGRepresentation(icon) forKey:#"image"]; // iOS
return newCat;
}
Swift function to save the MOC. Call this whenever you need to make sure things are written to disk.
// MARK: - Core Data Saving support
func saveContext () {
if let moc = self.managedObjectContext {
var error: NSError? = nil
if moc.hasChanges && !moc.save(&error) {
//FLOG("Unresolved error \(error), \(error!.userInfo)")
// Probably need a message to the user warning that a save failed!
}
}
}