Realm iOS Testing: In-Memory Databases and Mocking Realm Objects in Swift
Realm's object graph persistence is tightly coupled to its runtime — you can't just pass in a mock dictionary. Testing Realm-backed iOS code requires either an in-memory Realm configuration (fast, isolated, no file cleanup) or a purpose-built mock layer. This guide covers both approaches, plus migration testing, async Realm patterns, and structuring your code so Realm is replaceable in tests.
The Problem with Testing Realm Code
Realm stores data in a binary .realm file. Tests that hit the real file on disk cause several issues:
- Tests share state if the file isn't cleaned between runs
- Parallel test execution on the same file causes conflicts
- You can't inspect write transaction state without a live Realm instance
- Tests become slow and order-dependent
The solution is in-memory Realm configurations — Realm instances that behave identically to disk-backed ones, except data lives in memory and disappears when all references are released.
Setup: In-Memory Realm
import RealmSwift
extension Realm.Configuration {
static func makeTestConfiguration(identifier: String = UUID().uuidString) -> Realm.Configuration {
Realm.Configuration(
inMemoryIdentifier: identifier,
schemaVersion: 1 // match your production schema version
)
}
}Use this in test cases:
import XCTest
import RealmSwift
final class TaskRepositoryTests: XCTestCase {
var realm: Realm!
var repository: TaskRepository!
override func setUp() {
super.setUp()
realm = try! Realm(configuration: .makeTestConfiguration())
repository = TaskRepository(realm: realm)
}
override func tearDown() {
try! realm.write { realm.deleteAll() }
realm = nil
repository = nil
super.tearDown()
}
}Using UUID().uuidString as the identifier means each test class gets a fresh in-memory database. You can also use a fixed identifier if you want the database to persist across test methods within a class.
Defining Realm Models for Testing
class Task: Object {
@Persisted(primaryKey: true) var id: String = UUID().uuidString
@Persisted var title: String = ""
@Persisted var isCompleted: Bool = false
@Persisted var createdAt: Date = Date()
}Testing CRUD Operations
final class TaskRepositoryTests: XCTestCase {
// ... setUp/tearDown above
func testCreateTask() throws {
let task = Task()
task.title = "Write unit tests"
try repository.save(task)
let saved = realm.object(ofType: Task.self, forPrimaryKey: task.id)
XCTAssertNotNil(saved)
XCTAssertEqual(saved?.title, "Write unit tests")
XCTAssertFalse(saved?.isCompleted ?? true)
}
func testUpdateTask() throws {
let task = Task()
task.title = "Initial title"
try repository.save(task)
try repository.update(taskId: task.id, title: "Updated title")
let updated = realm.object(ofType: Task.self, forPrimaryKey: task.id)
XCTAssertEqual(updated?.title, "Updated title")
}
func testDeleteTask() throws {
let task = Task()
try repository.save(task)
XCTAssertNotNil(realm.object(ofType: Task.self, forPrimaryKey: task.id))
try repository.delete(taskId: task.id)
XCTAssertNil(realm.object(ofType: Task.self, forPrimaryKey: task.id))
}
func testFetchAllReturnsAllTasks() throws {
let titles = ["Task A", "Task B", "Task C"]
for title in titles {
let task = Task()
task.title = title
try repository.save(task)
}
let all = repository.fetchAll()
XCTAssertEqual(all.count, 3)
}
func testFetchCompletedOnlyReturnsCompleted() throws {
let completed = Task(); completed.title = "Done"; completed.isCompleted = true
let pending = Task(); pending.title = "Pending"; pending.isCompleted = false
try repository.save(completed)
try repository.save(pending)
let results = repository.fetchCompleted()
XCTAssertEqual(results.count, 1)
XCTAssertEqual(results.first?.title, "Done")
}
}The Repository Pattern: Making Realm Testable
The key to testable Realm code is injecting the Realm instance rather than accessing it as a singleton:
class TaskRepository {
private let realm: Realm
init(realm: Realm) {
self.realm = realm
}
func save(_ task: Task) throws {
try realm.write {
realm.add(task, update: .modified)
}
}
func fetchAll() -> Results<Task> {
realm.objects(Task.self)
}
func fetchCompleted() -> Results<Task> {
realm.objects(Task.self).where { $0.isCompleted == true }
}
func update(taskId: String, title: String) throws {
guard let task = realm.object(ofType: Task.self, forPrimaryKey: taskId) else { return }
try realm.write {
task.title = title
}
}
func delete(taskId: String) throws {
guard let task = realm.object(ofType: Task.self, forPrimaryKey: taskId) else { return }
try realm.write {
realm.delete(task)
}
}
}For production, pass try! Realm(). For tests, pass try! Realm(configuration: .makeTestConfiguration()).
Mocking Realm with a Protocol
When you can't use in-memory Realm (e.g., legacy code with singleton access), define a protocol:
protocol RealmProtocol {
func objects<Element: Object>(_ type: Element.Type) -> Results<Element>
func add(_ object: Object, update: Realm.UpdatePolicy)
func delete(_ object: Object)
func write(_ block: () throws -> Void) throws
}
extension Realm: RealmProtocol {}Then create a mock:
class MockRealm: RealmProtocol {
var savedObjects: [Object] = []
var deletedObjects: [Object] = []
func objects<Element: Object>(_ type: Element.Type) -> Results<Element> {
// Results<T> is hard to mock directly — use in-memory Realm instead
fatalError("Use in-memory Realm for query testing")
}
func add(_ object: Object, update: Realm.UpdatePolicy) {
savedObjects.append(object)
}
func delete(_ object: Object) {
deletedObjects.append(object)
}
func write(_ block: () throws -> Void) throws {
try block()
}
}Note: Results<T> is a live Realm query — it's not constructible outside Realm, so this pattern is best for testing write operations. For read testing, in-memory Realm is superior.
Testing Realm Queries
For complex query logic, test directly against in-memory Realm:
func testSortedByCreationDate() throws {
let old = Task(); old.title = "Old"; old.createdAt = Date(timeIntervalSince1970: 1000)
let new = Task(); new.title = "New"; new.createdAt = Date()
try repository.save(old)
try repository.save(new)
let sorted = realm.objects(Task.self).sorted(byKeyPath: "createdAt", ascending: false)
XCTAssertEqual(sorted.first?.title, "New")
}
func testFilterByTitle() throws {
let task = Task(); task.title = "Buy groceries"
try repository.save(task)
let results = realm.objects(Task.self).where { $0.title.contains("grocery") }
XCTAssertEqual(results.count, 0)
let caseless = realm.objects(Task.self).filter("title CONTAINS[c] %@", "groceries")
XCTAssertEqual(caseless.count, 1)
}Testing Migrations
Test migration blocks by configuring an older schema version against an in-memory Realm:
func testMigrationAddsDefaultPriority() throws {
// Simulate schema version 0 data
let oldConfig = Realm.Configuration(
inMemoryIdentifier: "migration-test",
schemaVersion: 0
)
// Create a Realm without the migration (old schema)
// In real migration tests, you'd use a pre-seeded .realm file
let newConfig = Realm.Configuration(
inMemoryIdentifier: "migration-test",
schemaVersion: 1,
migrationBlock: { migration, oldSchemaVersion in
if oldSchemaVersion < 1 {
migration.enumerateObjects(ofType: Task.className()) { _, newObject in
newObject?["priority"] = "normal" // default value for new field
}
}
}
)
let realm = try Realm(configuration: newConfig)
XCTAssertNotNil(realm)
// Migration ran without throwing — success
}For production migration testing, keep versioned .realm fixture files in your test bundle and apply migrations against them.
Testing Async Realm (Swift Concurrency)
RealmSwift supports async/await with actor-isolated access:
@MainActor
func testAsyncFetch() async throws {
let config = Realm.Configuration.makeTestConfiguration()
let realm = try await Realm(configuration: config)
try await realm.asyncWrite {
let task = Task()
task.title = "Async task"
realm.add(task)
}
let tasks = realm.objects(Task.self)
XCTAssertEqual(tasks.count, 1)
XCTAssertEqual(tasks.first?.title, "Async task")
}Mark the test method async throws and use await Realm(configuration:) for actor-safe initialization.
Testing Realm Notifications
Realm objects support change notifications. Test them with expectations:
func testTaskChangeNotification() throws {
let task = Task()
task.title = "Original"
try realm.write { realm.add(task) }
let expectation = XCTestExpectation(description: "Change notification received")
var token: NotificationToken?
token = task.observe { change in
if case .change(_, let properties) = change {
let titleChange = properties.first { $0.name == "title" }
XCTAssertNotNil(titleChange)
XCTAssertEqual(titleChange?.newValue as? String, "Updated")
expectation.fulfill()
}
}
try realm.write { task.title = "Updated" }
wait(for: [expectation], timeout: 1.0)
token?.invalidate()
}Summary
| Scenario | Approach |
|---|---|
| CRUD operations | In-memory Realm with injected configuration |
| Write-only behavior | MockRealm conforming to RealmProtocol |
| Complex queries | In-memory Realm with seeded data |
| Migration correctness | Fixture .realm files + migration block |
| Async/actor-isolated Realm | await Realm(configuration:) in async test |
| Notifications | observe() with XCTestExpectation |
The in-memory Realm pattern covers 90% of cases. Keep Realm injection in your initializers, avoid singletons, and your persistence layer becomes straightforward to test.