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.
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'm attempting to update an entity in CoreData. Here are some of my declarations.
static var appDelegate = UIApplication.shared.delegate as! AppDelegate
static var context = appDelegate.persistentContainer.viewContext
static var entity = NSEntityDescription.entity(forEntityName: "PData", in: context)
static var newPData = NSManagedObject(entity: entity!, insertInto: context)
I'm somewhat certain the fact that they're static isn't relevant.
PData is the name of the entity (short for persistent data).
Later on, I set the values I'd like to save using newPData.setValue("foo", forKey: "bar"), but when I actually attempt to save them with context.save(), I get NSCocoaErrorDomain Code 133020 "Could not merge changes."
I should mention that this is meant to occur directly after deleting an existing entry in PData (replacing an old entity instance with a new one).
I've done some reading, and I've found that the reason for this error is that the default way that Swift handles a CoreData merge conflict is to throw an error. I'd like to change my CoreData settings such that changes from memory overwrite changes already stored within the entity in CoreData, but I'm not sure as to how I'd going about doing this.
The Apple documentation shows lots of different merge policy options, but doesn't have an example showing how to implement them. I think the one I need to use is NSMergeByPropertyStoreTrumpMergePolicy, but I have no idea how to actually set said policy as the merge policy.
I found the answer - to set the context's merge policy, you simply do
context.mergePolicy = NSMergePolicy(merge: NSMergePolicyType.mergeByPropertyObjectTrumpMergePolicyType)
I was getting tripped up by trying to do
context.mergePolicy = mergeByPropertyObjectTrumpMergePolicyType)
but I guess it's necessary to spawn an NSMergePolicy object. I just assumed that the actual merge policy (mergeByPropertyObjectTrumpMergePolicyType) would be of the correct type by default.
You can put mergePolicy when initiate persistentContainer
var persistentContainer: NSPersistentContainer = {
let modelURL = Bundle.main.url(forResource: DB_NAME, withExtension: "momd")!
let container = NSPersistentContainer.init(name: DB_NAME, managedObjectModel: NSManagedObjectModel(contentsOf: modelURL)!)
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
if let error = error as NSError? {
QiscusLogger.errorPrint("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
Merge policies are used in coredata to resolve the conflict issue between persistent store and different managed object context, it depends on you which merge policy is suitable for your application.
As from the code snippet it seams that you are using single managed object conext.
Please try below code-
appDelegate.persistentContainer.viewContext.automaticallyMergesChangesFromParent = true
appDelegate.persistentContainer.viewContext.mergePolicy = NSMergePolicy.mergeByPropertyStoreTrump
In Swift 5 once you create a project with Core Data, then you have Persistence.swift file in your project directory.
In this class put mergePolicy code in the last of init() function.
init(inMemory: Bool = false) {
....
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
}
And it's work for me 😉.
I've designed a core data stack based on this blog post (in Swift) where I have two NSManagedObjectContext instances, a Main Queue Context (NSMainQueueConcurrencyType) and a Private Queue Context (NSPrivateQueueConcurrencyType) where the main context's job is to deal with all things user interaction related (editing, presenting data to the user) and the private context's only job is to write to disk.
To make managing the stack as easy as possible, I've integrated Magical Record, Overcoat, and Mantle. I've separated all this into two classes, a Core Data singleton stack (built on Magical Record) and a network manager singleton (Built on Overcoat which in turn is built on Mantle).
The Core Data stack looks like this:
import UIKit
import CoreData
import MagicalRecord
class CoreData: NSObject {
enum StackType: Int {
case Default, AutoMigrating, iCloud, inMemory
}
static let sharedStack = CoreData()
private override init() {}
var type: StackType = .Default
func setupStackWithType(type: StackType, withName name: String = MagicalRecord.defaultStoreName()) {
self.type = type
switch self.type {
case .Default:
MagicalRecord.setupCoreDataStackWithStoreNamed(name)
case .AutoMigrating:
MagicalRecord.setupCoreDataStackWithAutoMigratingSqliteStoreNamed(name)
case .iCloud:
if let documentsPath = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true).first {
let url = NSURL(fileURLWithPath: documentsPath)
MagicalRecord.setupCoreDataStackWithiCloudContainer(name, localStoreAtURL: url)
} else {
print("Error: could not find documents directory")
}
case .inMemory:
MagicalRecord.setupCoreDataStackWithInMemoryStore()
}
}
func setupStackWithStoreName(storeName: String, automigrating: Bool = true) {
if automigrating {
MagicalRecord.setupCoreDataStackWithAutoMigratingSqliteStoreNamed(storeName)
} else {
MagicalRecord.setupAutoMigratingCoreDataStack()
}
}
func saveWithBlock(block: (NSManagedObjectContext!) -> ()) {
MagicalRecord.saveWithBlock(block, completion: {
(success, error) in
})
}
func cleanUp() {
MagicalRecord.cleanUp()
}
var managedObjectContext: NSManagedObjectContext {
return NSManagedObjectContext.MR_defaultContext()
}
var privateContext: NSManagedObjectContext {
return NSManagedObjectContext.MR_rootSavingContext()
}
var coordinator: NSPersistentStoreCoordinator {
return NSPersistentStoreCoordinator.MR_defaultStoreCoordinator()
}
var persistentStore: NSPersistentStore {
return NSPersistentStore.MR_defaultPersistentStore()
}
}
My network manager looks like:
import UIKit
import Overcoat
import MTLManagedObjectAdapter
class NetworkManager: OVCManagedHTTPSessionManager {
static let singleton = NetworkManager(baseURL: NSURL(string: Config.ServerBaseEndpoint), managedObjectContext: nil, sessionConfiguration: {
let config = NSURLSessionConfiguration.defaultSessionConfiguration()
config.timeoutIntervalForRequest = 15
config.timeoutIntervalForResource = 15
return config
}())
private override init(baseURL url: NSURL?, managedObjectContext context: NSManagedObjectContext?, sessionConfiguration configuration: NSURLSessionConfiguration?) {
super.init(baseURL: url, managedObjectContext: context, sessionConfiguration: configuration)
self.responseSerializer.acceptableContentTypes = ["text/html", "application/json", "application/xml", "image/png"]
self.securityPolicy = AFSecurityPolicy(pinningMode: .None)
self.securityPolicy.allowInvalidCertificates = true
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
// MARK: - OVCHTTPSessionManager
override class func modelClassesByResourcePath() -> [String: AnyClass] {
return [Types.RestApi.Post.rawValue:Post.self, "\(Types.RestApi.Post.rawValue)/*": Post.self]
}
}
What I can't quite wrap my head around is 1) how these two classes can work in conjunction and 2) in regards to the core data stack, which context to save on, what work do on on which context, etc.
For NetworkManager.swift (which takes a NSManagedObjectContext to persist models to):
Which context do I initialize the manager with? My assumption would be if you make a network request and that JSON gets transformed into intermediate Mantle models and from there into NSManagedObejct instances, those instances should be saved right to the Private Queue Context, bypassing the Main Queue Context altogether in this instance.
When talking about CoreData.swift:
1) Magical Record has the saveWithBlock method which creates a local context and propagates it up to the root context (in this case the root context is the Private Queue Context) but it's unclear to be what work should be done inside the block.
In their documentation, they give this example:
Person *person = ...;
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext){
Person *localPerson = [person MR_inContext:localContext];
localPerson.firstName = #"John";
localPerson.lastName = #"Appleseed";
}];
where they create a Person object outside of the save block and then re-make the entity in the local context then edit it's properties. However in my case, all Person objects would be an instance of MTLModel, not NSManagedObject. When I create some model object, since it isn't a core data object, it wouldn't be inserted into any sort of context until I use MTLManagedObjectAdapter to transform the model into an NSManagedObject instance.
The best way to go about it seems to be create the MTLModel instance(s), do whatever editing is needed, and then either 1) inside saveWithBlock insert the newly created managed objects right into the local context and let it propagate up or 2) insert the objects into the Private Queue Context and save.
2) Do I really need to use the Main Queue Context at all for saving and editing? As I said before, Mantle has the model classes as a subclass of MTLModel and later maps them into NSManagedObject instances so it makes sense that I could just save directly to the Private Queue Context (whose only job is to write to disk anyway)
3) If I don't need to use the Main Queue Context for saving/editing, wouldn't it become the context I use for fetching NSManagedObjects (given that the Private Queue's job is to write to disk and the saving/editing functions of the Main Queue Context seem to be made obsolete by Mantle's intermediate model structure)?
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