Core data temporary database for unit tests - ios

Is this possible to create new temporary core data database when I run tests?
Because I have a problem, when I run my test I create wishlist:
import XCTest
#testable import TestProj
class ChangeWishListTests: XCTestCase {
func testSaveWishList() {
self.wishList = self.changeWishListVC?.saveWishList(title: "Test wish list",
desc: "My description",
wishlistType: WishListType.Shared,
hidden: false)
XCTAssertNotNil(wishList, "Wishlist not created.")
}
}
Than it appears in simulator. Or if it is impossible, how can I manage my fake objects.

Yes, you can but in order to do that you have to be able to change (tell the VC) managed object context that is used to perform Core Data operations. By doing you can then user test managed object in your test and the true one in production application code.
By test managed object context I mean the one that stores data only in memory without saving anything to the disc - the results of operations performed on that kind of context aren't persisted between different launches of the tests.
Creating managed object context that stores data only in memory is pretty simple:
let managedObjectModel = NSManagedObjectModel.init(contentsOfURL: modelURL)
var managedObjectContext = NSManagedObjectContext.init(concurrencyType: .MainQueueConcurrencyType)
managedObjectContext.persistentStoreCoordinator = NSPersistentStoreCoordinator.init(managedObjectModel: managedObjectModel)
var error: NSError?
let options = [NSMigratePersistentStoresAutomaticallyOption : true, NSInferMappingModelAutomaticallyOption : true];
let persistentStore = try! managedObjectContext.persistentStoreCoordinator?.addPersistentStoreWithType(NSInMemoryStoreType, configuration: nil, URL: nil, options: options)
The simplest way to give yourself possibility to use test managed object context is to use dependency injection. Create initializer of your VC that takes managed object context as an argument - inject test managed object context in test code and normal managed object context in production code.

Related

Create CoreData object in tests with a simple init

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)

How to use Core Data for main app and unit tests?

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 :)

How to set merge policy in Swift 4 CoreData

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

Swift 4 Unit Testing with Core Data

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.

How to access NSManagedObject entity using objectID which is not saved yet in iOS?

I have created one entity using private queue MOC and called performBlock() to save it asynchronously.
Let’s say I want this newly created entity in other context / MOC, so I have only one way to access it is use object id.
But as it’s asynchronous call to save entity, it is possibility of situation where entity is not saved yet and I want to access it immediately after performBlock(), but on other context.
What is the way to access it in this situation?
Thanks in advance!
Update
let privateMOC = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
privateMOC.parentContext = mainThreadManagedObjectContext()
let newUser = Contact(nameStr: "name", moc: privateMOC)
This is my generic code to create new entities like:
init(nameStr: String, moc:NSManagedObjectContext) {
let mEntity = NSEntityDescription.entityForName("Contact", inManagedObjectContext: moc)
super.init(entity: mEntity!, insertIntoManagedObjectContext: moc)
name = nameStr
}
After creating, I am saving it immediately as:
performBlock { () -> Void in
do {
try privateMOC.save()
}catch {
fatalError("Error asynchronously saving private moc - \(errorMsg): \(error)")
}
}
//accessing
let privateMOC = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
privateMOC.parentContext = mainThreadManagedObjectContext()
let pred = NSPredicate(format: "name == %#", "name")
if let results = DatabaseInterface.sharedInstance.fetchEntityFromCollection("Contact", withPredicate: pred, moc: privateMOC) {
if let user = results.last {
return user as! Contact
}
}
else {
DDLogWarn("No user found")
}
I am not getting any user, but I can see this new contact is saved in DB when look using sqlitebrowser
Forgive me for indentation of code :(
What you're asking is impossible.
When you create a managed object on one context, it only exists in memory until you save changes. No other managed object context can get to it, even if you use the object ID. The other context can't look up the data from the persistent store file, and it can't get it from the other context. It can't get the object at all. You need to wait until the save completes.
A couple of things you might try, depending on how your app is structured:
Use performBlockAndWait instead of performBlock, so that you'll know that the save has completed by the time the block finishes.
Keep using performBlock but put calls inside that block that lead to accessing the object on a different context-- via a nested performBlock call on that other context.

Resources