Edited at

【iOS】RealmをRx化してQuick/Nimble/RxBlockingでテストする


Realm


:computer:環境構築



  • 今回はCarthageを使用してライブラリを使用する

github "realm/realm-cocoa"

carthage update を実施し Linked Frameworks and Libraries に次のframeworkを追加する。


  • RealmSwift.framework

  • Realm.framework

Build PhasesNew Run Script Phase で スクリプトとInput Files に次を追加する。



  • スクリプト

    /usr/local/bin/carthage copy-frameworks
    



  • Input Files

    $(SRCROOT)/Carthage/Build/iOS/Realm.framework
    
    $(SRCROOT)/Carthage/Build/iOS/RealmSwift.framework



:pencil: 実装



モデルの作成


  • 何個かサンプルでモデルを作成

Userモデル

import RealmSwift

import Foundation

class User: Object {
@objc dynamic var id = -1
@objc dynamic var name = ""
let diaries = List<Diary>()
}

Diaryモデル (Userモデル hasMany Diaryモデル)

import RealmSwift

import Foundation

class Diary: Object {
@objc dynamic var user = User?
@objc dynamic var content = ""
@objc dynamic var date = Date()
}



Realm + Rx

RxSwiftCommunityRxRealmが既に存在していますが、

今回は勉強も兼ねて一からRealmにRxを組み込んで実装してみたいと思います。

最終的にできたのがこちら

import Foundation

import RxSwift
import RealmSwift

public extension Realm {

/// find all: 全要素を返す
static func all<T>(type: T.Type) -> Observable<[T]> where T : Object {
return Observable.create { observer in
do {
let realm = try Realm()
let objects = realm.objects(type)
observer.onNext(Array(objects))
observer.onCompleted()
} catch let error {
observer.onError(error)
}
return Disposables.create()
}
}

/// query: 検索 + ソート、降順/昇順を指定
/// 1件ずつ検索結果を返す
static func query<T>(
type: T.Type, formattedString: NSPredicate, sortedKey: String = "", ascending: Bool = true) -> Observable<T> where T : Object {
return Observable.create { observer in
do {
let realm = try Realm()
let objects = realm.objects(type).filter(formattedString)
let items = Array(sortedKey.isEmpty ? objects :
objects.sorted(byKeyPath: sortedKey, ascending: ascending))
items.forEach { observer.onNext($0) }
observer.onCompleted()
} catch let error {
observer.onError(error)
}
return Disposables.create()
}
}

/// insert object: 新規登録 or 更新処理
static func add<T>(element: T, update: Bool = false) -> Observable<()> where T : Object {
return Observable.create { observer in
do {
let realm = try Realm()
try realm.write {
// NOTE: 既に存在する場合、変更箇所のみupdate, 存在しない場合は登録する
realm.add(element, update: .modified)
observer.onNext(())
observer.onCompleted()
}
} catch let error {
observer.onError(error)
}
return Disposables.create()
}
}
}

ひとまず、全件を取得する all と検索やソート指定を行う query

登録処理を行う add を実装しました。

実際に使う場合は以下の様な感じになります。

// 全ユーザーを取得

Realm.all(type: User.self)
.subscribe(onNext: { users in ...

// 特定のユーザーを検索
let name = "..."
Realm.query(type: User.self, formattedString: NSPredicate(format: "name == %@",
argumentArray: [name]), sortedKey: "id")
.toArray()
.asObservable()
.subscribe(onNext: { users in ...

// ユーザーを登録
let user = User()
...
let diary1 = Diary()
diary1.user = user
user.diaries.append(diary1)
let diary2 = Diary()
diary2.user = user
user.diaries.append(diary2)

Realm.add(element: user)
.subscribe(...


:eyes: テストを書いてみる


今回QuickとNimble, RxBlockingを使用してテストを書いてみます。



テスト用のdatabaseを作成し、テストではそちらを使う様にしたい為、

Helperメソッドを作成。


QuickHelper.swift

import Quick

import Nimble
import RealmSwift

public extension QuickSpec {

func createTestRealm() -> Realm {
var config = Realm.Configuration()
var url = config.fileURL!
url.deleteLastPathComponent()
config.fileURL = url.appendingPathComponent("test.realm")
Realm.Configuration.defaultConfiguration = config
let realm = try! Realm(configuration: config)

print(Realm.Configuration.defaultConfiguration.fileURL!)

return realm
}
}


単純に通常作成される default.realm の代わりにテストの時は test.realm を作成し、

そちらを使う様に変更。

上記のHelperを使って、最終的には以下の実装になりました。

import Quick

import Nimble
import RealmSwift
import RxBlocking
@testable import XXXXXXX

class RealmRxSpec: QuickSpec {
override func spec() {

// テスト用のrealmを作成
let realm = createTestRealm()

// MARK: - Realm.all

describe("all method test") {
beforeEach {
let user1 = User()
user1.id = 1
user1.name = "name1"
try! realm.write {
realm.create(User.self, value: user1, update: .all)
}

let user2 = User()
user2.id = 2
user2.name = "name2"
try! realm.write {
realm.create(User.self, value: user2, update: .all)
}
}

it("should be find all result") {
let result = try! Realm.all(type: User.self)
.toBlocking()
.single()
expect(result.count).to(equal(2))
}
}

// MARK: - Realm.query

describe("query method test") {
beforeEach {
let user1 = User()
user1.id = 1
user1.name = "name1"
try! realm.write {
realm.create(User.self, value: user1, update: .all)
}

let user2 = User()
user2.id = 2
user2.name = "name2"
// diary
let diary1 = Diary()
diary1.user = user
diary1.content = "diary1"
let diary2 = Diary()
diary2.user = user
diary2.content = "diary1"
user2.diaries.append(diary1)
user2.diaries.append(diary2)
try! realm.write {
realm.create(User.self, value: user2, update: .all)
}
}

it("should be query result") {
let results = try! Realm.query(type: User.self, formattedString:
NSPredicate(format: "name == %@", argumentArray: ["name2"]),
sortedKey: "id", ascending: false)
.toBlocking()
.toArray()
expect(results.count).to(equal(1))
expect(results.diaries.count).to(equal(2))
}
}

afterEach {
// テスト終了後にデータを全て削除
try! realm.write {
realm.deleteAll()
}
}
}
}


:link: 参考になったURL