"Cannot modify managed RLMArray outside of a write transaction." in XCTest - ios

I'm attempting to write some unit tests for my view models that interact with Realm. The logic works fine when run on device / simulator, but triggers a ""RLMException", "Cannot modify managed RLMArray outside of a write transaction" when unit tested.
My test case is as follows...
func testThatNewlyAddedPaymentsAreReturned() throws {
let payment = Payment(recipient: "Matt", amount: Decimal(1.0), date: Date(), note: "")
try model.addPayment(payment: payment) // Throws exception
XCTAssertTrue(model.payments?.contains(payment) ?? false)
}
In the test case above, the model variable is the view model class, which has a simple one line implementation...
func addPayment(payment: Payment) throws {
try self.budget?.addPayment(payment: payment)
}
This in turn calls through to the Budget class where the Realm interactions take place.
func addPayment(payment: Payment) throws {
let realm = try Realm()
try realm.write {
_payments.append(payment)
}
}
Note that contrary to the exception message the private var _payments = List<Payment>() property is being modified within a Realm write transaction.
I've configured the default Realm configuration in the unit test as follows...
override func setUp() {
var config = Realm.Configuration.init()
// Set this as the configuration used for the default Realm
Realm.Configuration.defaultConfiguration = config
config.inMemoryIdentifier = "BudgetTests"
try! repository = BudgetRepository.init(realm: Realm(configuration: config))
try! initialiseViewModel()
}

Updating the test setup to initialise the realm using the no-args initializer fixed the issue e.g.
Replacing...
try! repository = BudgetRepository.init(realm: Realm(configuration: config))
... with ...
try! repository = BudgetRepository.init(realm: Realm())
Oddly this would appear to imply that opening a Realm with the default configratuion e.g. Realm() does not produce the equivalent Realm as initializating one and manually supplying the default configuration.

Related

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 migrate multiple realm files in one App

I have two realm files in one App, something wrong when I wanna migrate them. I want realm update automatically when run in Xcode while does not change the schemaVersion every time.
class News: Object {
#objc dynamic var name: String
}
class NewsManager {
static var realm = try! Realm()
static var cacheRealm: Realm = {
let documentDirectory = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask,
appropriateFor: nil, create: false)
let url = documentDirectory.appendingPathComponent("cache.realm")
var config = Realm.Configuration()
config.fileURL = url
return try! Realm(configuration: config)
}()
}
When I add a new property to News such as #objc dynamic var title: String,
I add the following code in AppDelegate func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool
let config = Realm.Configuration(schemaVersion: 1, migrationBlock: { migration, oldSchemaVersion in
})
Realm.Configuration.defaultConfiguration = config
The message on crash at return try! Realm(configuration: config) in NewsManager.
Fatal error: 'try!' expression unexpectedly raised an error: Error Domain=io.realm Code=10 "Migration is required due to the following errors:
- Property 'News.test' has been added." UserInfo={Error Code=10, NSLocalizedDescription=Migration is required due to the following errors:
- Property 'News.test' has been added.}: file /BuildRoot/Library/Caches/com.apple.xbs/Sources/swiftlang/swiftlang-900.0.69.2/src/swift/stdlib/public/core/ErrorType.swift, line 181
What should I do?
Realm: 3.0.1
Swift: 4.0
iOS : 10
Realm is working as expected. Some types of model changes Realm can migrate automatically, others require you to provide a manual migration. The one in your example is one of them.
Since you added a new, non-optional property of type String, Realm needs to go through all your existing models and figure out what to put in that property. If the property were type String?, it would make sense to use nil as a default value, and Realm could carry out the migration automatically. However, since the type is String and there's no obvious sensible default, Realm requires you to manually specify a value for the new property for each model object.
The right way to fix this "problem" is to increment your schema number and provide a migration block that actually specifies the new values for new properties whenever you change your model in a way that requires a migration.
Please review our documentation on migrations for more details.
Check it out https://realm.io/docs/swift/latest/#migrations. Realm site is very good for understanding how properly use Realm in your projects

About transaction in Realm database

This is my Realm object:
class AchievementDate : Object {
dynamic var date: Date = Date()
dynamic var apple: Int = Int(0)
func save() {
do {
let realm = try Realm()
try realm.write {
realm.add(self)
}
} catch let error as NSError {
fatalError(error.localizedDescription)
}
}
}
I change apple's value in View controller's viewDidLoad() method, as you can see:
override func viewDidLoad() {
super.viewDidLoad()
achievementDate.apple = 2
achievementDate.save()
}
then I update the apple's value when user clicks the pause button on the screen like this:
#IBAction func pausedButtonTapped(_ sender: UIButton) {
achievementDate.apple += 1
achievementDate.save()
}
Xcode runs it successfully but when I click the pause button, the app crashes. In the console it says:
*** Terminating app due to uncaught exception 'RLMException', reason: 'Attempting to modify object outside of a write transaction - call
beginWriteTransaction on an RLMRealm instance first.'
I am quite confused about this, btw, what does transaction mean in general? Thanks a lot.
A write transaction is used to group modifications of objects within a Realm into a single unit of work. Managed Realm objects may only be modified within a write transaction. The write transaction is scoped to the block you pass to the call to Realm.write(_:). The call to write begins a write transaction, the body is executed with the transaction active, and when the block returns the write transaction is committed and the changes are persisted to the Realm file.
You've not shared how achievementDate is initialized, but it seems safe to assume based on the exception you're seeing that it is an AchievementDate instance that is a managed object (that is, it was either created then added to a Realm, or it was loaded from a Realm). As the exception notes, you can only modify managed objects within a write transaction. You can either expand the scope of your write transaction to encompass the modification of the managed object, or you can avoid modifying a managed object altogether (by adding a primary key to your model class, and using either Realm.create(_:value:update:) or Realm.add(_:update:) with update: true to update an existing object with the given primary key value).
First, you need to define a Primary Key if you want to update the model:
class AchievementDate: Object {
dynamic var id = 0
dynamic var date:Date = Date()
dynamic var apple:Int = Int(0)
override static func primaryKey() -> String? {
return "id"
}
}
Then update the model like:
static func save(achievementDate:AchievementDate) {
let realm = try! Realm()
try! realm.write {
realm.add(achievementDate, update: true)
}
}
Alternatively, if you don't want primary Key, and the model is already retrieved from Realm, you could update the model like this:
let realm = try! Realm()
try! realm.write {
achievementDate.apple = 2
}

Design pattern for Realm persistence

I'm using Realm in a App and I'm trying to abstract much as possible so that in the future I can swap database providers without too much change.
This pattern has worked well although I'm concerned about the following.
Is creating a new realm object everytime a overhead (My current understanding is that Realm Objects are cached internally)?
Are there any problems with the way I'm using Realm ?
Are there any better design patterns for my purpose ?
public struct BookDataLayer: BookDataLayerProvider {
func isBookAvailable(bookIdentifier: String) throws -> Bool {
let database = try getDatabase()
return !database.objects(Book).filter("identifier = %#", bookIdentifier).isEmpty
}
func createOrUpdateBook(bookIdentifier: String, sortIndex: Int) throws {
let book = Book()
Book.bookIdentifier = bookIdentifier
Book.sortIndex = sortIndex
try create(book, update: true)
}}
protocol BookDataLayerProvider : DataAccessLayer {
func isBookAvailable(bookIdentifier: String) throws -> Bool
func createOrUpdateBook(bookIdentifier: String, sortIndex: Int) throws n}
extension DataAccessLayer {
func getDatabase() throws -> Realm {
do {
let realm = try Realm()
// Advance the transaction to the most recent state
realm.refresh()
return realm
} catch {
throw DataAccessError.DatastoreConnectionError
}
}
func create(object: Object, update: Bool = false) throws {
let database = try self.getDatabase()
do {
database.beginWrite()
database.add(object, update: update)
// Commit the write transaction
// to make this data available to other threads
try database.commitWrite()
} catch {
throw DataAccessError.InsertError
}
}}
// Usage
let bookDataLayer = BookDataLayer()
bookDataLayer.isBookAvailable("4557788")
bookDataLayer.createOrUpdateBook("45578899", 10)
That's a completely solid design pattern. It's pretty common for developers to abstract the data layer APIs way from their code in case they need to switch it out.
In response to your questions:
You're correct. Realm object instances are internally cached, so you can easily call let realm = try! Realm() multiple times with very little overhead.
Unless you've found a specific reason, it's probably not necessary to call refresh() on the Realm instance every time you use it. Realm instances on the main thread are automatically refreshed on each iteration of the run loop, so you only need to call refresh() if you're expecting changes on a background thread, or need to access changes before the current run loop has completed.
'Better' design patterns is probably a matter of opinion, but from what I've seen from other codebases, what you've got there is already great! :)

Realm and Parse integration: how to save the realm

I'm trying to create a bundle realm for my application. I thought it should be quite simple. Since I already have all needed records in Parse, I just have to:
create realm models and objects
load parse records to realm objects
save the realm
So, I created two realm models:
class RealmContainer : Object {
dynamic var rContainerId: String! //should be equal objectId from Parse
dynamic var rContainerName: String! //should be equal "name" field from Parse
...
var messages = List<RealmMessage>()
override static func primaryKey() -> String? {
return "rContainerId"
}
}
and
class RealmMessage : Object {
dynamic var rMessageId: String!
...
dynamic var rParentContainer: RealmContainer!
}
Getting results from Parse seems to be working. Also my realm objects are also good
var allUserContainers: [RealmContainer] = []
I was able to populate this array with values from Parse. But when I'm trying to save this realm, I'm getting a) nothing or b) error message
My code (this one'll get nothing):
let realm = try! Realm()
try! realm.write {
realm.add(self.allUserContainers[0])
print(Realm().path)
print(realm.path)
}
My code (this one'll get nothing too):
let realm = try! Realm()
try! realm.write {
realm.create(RealmContainer.self, value: self.allUserContainers[0], update: true)
print(Realm().path)
print(realm.path)
}
My code 3 (this will get me an error message "Terminating app due to uncaught exception 'RLMException', reason: 'Illegal recursive call of +[RLMSchema sharedSchema]. Note: Properties of Swift Object classes must not be prepopulated with queried results from a Realm"):
//from firstViewController, realm is a global variable
let realm = try! Realm()
//another swift module
try! realm.write {
realm.create(RealmContainer.self, value: self.allUserContainers[0], update: true)
print(Realm().path)
print(realm.path)
}
Obviously I don't properly understand how it should work, but I tried several swift/realm tutorials and they were actually straightforward. So, what did I do wrong?
Update
So, I updated my code to make it as much simple/readable as possible. I have a Dog class, and I am trying to get Dogs from Parse and put them to Realm.
AppDelegate.swift
let realm = try! Realm() //global
Dog.swift
class Dog : Object {
dynamic var name = ""
dynamic var age = 0
}
User.swift (getDogs and putDogs functions)
class User {
var newDogs:[Dog] = []
...
func getDogs() {
self.newDogs = []
let dogsQuery = PFQuery(className: "Dogs")
dogsQuery.limit = 100
dogsQuery.findObjectsInBackgroundWithBlock { (currentModes, error) -> Void in
if error == nil {
let tempModes:[PFObject] = currentModes as [PFObject]!
for var i = 0; i < tempModes.count; i++ {
let temp = Dog()
temp.name = currentModes![i]["dogName"] as! String
self.newDogs.append(temp)
}
} else {
print("something happened")
}
}
}
...
func putDogs() {
print(self.newDogs.count)
try! realm.write({ () -> Void in
for var i = 0; i < newDogs.count; i++ {
realm.add(newDogs[i])
}
})
try! realm.commitWrite() //doesn't change anything
}
error message still the same:
Terminating app due to uncaught exception 'RLMException', reason:
'Illegal recursive call of +[RLMSchema sharedSchema]. Note: Properties
of Swift Object classes must not be prepopulated with queried
results from a Realm
I believe I just have some global misunderstanding about how Realm is working because it is extremely simple configuration.
About your RealmSwift code : You have implemented it right.
When you declare a class of realm in swift, it's a subclass of Object class. Similarly for the parse it's subclass of PFObject.
You custom class have to have only one base class. So can't use functionalities of both the libraries Parse as well as Realm.
If you have to have use both Parse and Realm, you need to declare two different classes like RealmMessage and ParseMessage.
Retrieve data for ParseMessage from parse and copy properties to RealmMessage.
Why you want to use both Parse and Realm ?
parse also provides local data store by just writing Parse.enableLocalDatastore() in appDelegate before
Parse.setApplicationId("key",
clientKey: "key")
Your Realm Swift code seems fine. Realm is very flexible when creating objects using (_:value:update:), in being able to take in any type of object that supports subscripts. So you could even directly insert PFObject if the schemas matched.
The problem seems to be how you're populating the allUserContainers array. As the error says, you cannot fetch a Realm Object from Realm and then try and re-add it that way. If you're trying to update an object already in Realm, as long as the primary key properties match, you don't need to supply the whole object back again.
Can you please revisit the logic of how your allUserContainers variable is being populated, and if you can't fix it, post the code into your question?
Sidenote: You probably don't need to define your Realm properties as implicitly unwrapped as well. This is the recommended pattern:
class RealmContainer : Object {
dynamic var rContainerId = ""
dynamic var rContainerName = ""
}
Actually I found what was wrong: as i suspected it was a stupid mistake, some property in my class was UIImage type, and Realm just can't work with this type (even if I'm not trying to put objects of this class into realm). So, my bad. I am slightly embarassed by it, but will not delete my question because error messages from Realm were not very understandable in this case

Resources