19

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:

enter image description here

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?

3

5 Answers 5

25

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.

  1. Set your test target Host Application and check "Allow testing Host Applications APIs".

enter image description here

  1. 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.

enter image description here

  1. 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.

Sign up to request clarification or add additional context in comments.

Comments

6

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:

enter image description here

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
}()

5 Comments

This is the solution working for me, and I put this piece code in AppDelegate to make it work. For some reason, I am not able to make it to work when I used it in my unit tests directly. And Oliver Shaw's answer only works for non Tests target and don't use it together with this solution because I found it will make this solution failed to work.
I love the idea and see how it could work.... as long as you don't use an NSPersistentDocument on MacOS. Problem seems to be that NSPersistentDocument comes with a context, already ready with a model. Initialisation of an NSManagedObject descendent then fails, I assume because internal model doesn't know the entity. :-(
@Ludovic Landry Thank you so much for great direction. However, it does not work in my environment (XCode version 6.2). I would be grateful you could suggest me what should I do. I posted a related question with my code. Thanks. stackoverflow.com/questions/29313645/…
This is not needed and combursome. Use @objc(Classname) before your NSManagedObject subclass. The answer of @Oliver Shaw is correct.
Excellent - works for me too. I don't find it particularly cumbersome and the problem I see with Oliver's solution is that there will be other cases where we need to access the NSManagedObject types (e.g. casting the results of fetch requests) and I don't see any equivalent fix for these cases (ready to be proved wrong though...).
5

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
    ...
}

1 Comment

I can see how this works for creating new entities. But what about if you need to use casts later, for example with fetch requests, which will return entities that you need to cast into the appropriate types?
1

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
}

Comments

1

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 = "[email protected]"
        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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.