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

## Realm

<a href="https://github.com/realm/realm-cocoa"><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299271/11c8a401-06e2-82c9-cd0f-afe11d058221.png" width="460px"></a>

###  :computer:環境構築
****

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

```Cartfile
github "realm/realm-cocoa"
```

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

- `RealmSwift.framework`
- `Realm.framework`

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

- スクリプト

    ```sh
    /usr/local/bin/carthage copy-frameworks
    ```

- Input Files

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

### :pencil: 実装
****

#### モデルの作成

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

Userモデル

```swift
import RealmSwift
import Foundation

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

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

```swift
import RealmSwift
import Foundation

class Diary: Object {
    @objc dynamic var user = User?
    @objc dynamic var content = ""
    @objc dynamic var date = Date()
}
```
----
#### Realm + Rx

[RxSwiftCommunity](https://github.com/RxSwiftCommunity)に[RxRealm](https://github.com/RxSwiftCommunity/RxRealm)が既に存在していますが、
今回は勉強も兼ねて一からRealmにRxを組み込んで実装してみたいと思います。

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

```swift
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` を実装しました。
実際に使う場合は以下の様な感じになります。

```swift
// 全ユーザーを取得
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を使用してテストを書いてみます。

<a href="https://github.com/Quick/Quick"><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299271/25405d2a-5bb4-2098-3597-bb04d601f8fa.png" width="460px"></a>
<a href="https://github.com/Quick/Nimble"><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299271/5fb0f68f-de5a-3ad6-9380-e04c959ec6c3.png" width="460px"></a>


テスト用のdatabaseを作成し、テストではそちらを使う様にしたい為、
Helperメソッドを作成。

```swift: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を使って、最終的には以下の実装になりました。

```swift
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
****

- [SwiftでRealm Databaseを使ってみた - Qiita](https://qiita.com/ottijp/items/35dee1c03e055e450c81)
- [RealmのResults型をArrayに変換する - プログラミングガール](http://hana-ori.hatenablog.com/entry/2016/04/10/191435)