RxTest、RxBlockingによるテストパターン

RxTestとRxBlocking、プロジェクトにRxSwift導入している人はみんな使っていますよね:question:

私の関わっているプロジェクトでもRxSwiftを使用しており、もちろんテストコードを書く際にはこの2つのライブラリを使っています。
RxTestとRxBlockingを使ったテストコードの書き方にはいくつか基本的なパターンがあり、それらを覚えておけば、Rxなコードのテストは簡単にかけるはずです。

本記事では、RxTestとRxBlockingを使ったテストコードの基本パターンと、実際のプロジェクトで出てくるようなプロダクションコードに対してそれらのパターンをどう適用するかという具体例を説明します。

なお、本記事で紹介するコードはRxSwift/RxCocoa/RxTest/RxBlockingのいずれもバージョン`4.1.2`で動作確認しています。
また、テストコードの記述にはQuick、Nimbleを使用しています。

掲載しているコードは以下のリポジトリに全て含まれています。
実際にテストを動かして動作確認してみてください。

https://github.com/takehilo/RxSwiftTestPatterns

そもそもRxとは？RxSwiftとは？という方は、まずこれらの記事から読んでみると良いのではないでしょうか。

- [オブザーバーパターンから始めるRxSwift入門](https://qiita.com/k5n/items/17f845a75cce6b737d1e)
- [RxSwift の Observable とは何か](https://qiita.com/gomi_ningen/items/c796c08fe672610beecf)

## RxTest、RxBlockingとは
RxSwiftを利用していると、入力や戻り値の型に`Observable`型をとる関数が出てきます。
通常の関数のテストであれば、何か入力を与えて関数を実行し、その結果を検証するということができますが、戻り値の型が`Observable`型である関数は通常の関数のようにテストすることは出来ません。
こうしたRxSwift特有のコードをテストするための便利な機能を提供してくれるのが、RxTestおよびRxBlockingです。

### RxTest
RxTestが提供する`TestScheduler`は、指定した時刻にObservableにイベントを発行することができるスケジューラです。
`TestScheduler`を利用することで、どの時刻にどんなイベントがオブザーバに届いたのかを検証することができるようになります。

また、RxTestを使う上でおさえておきたいものとして、`HotObservable`と`ColdObservable`があります。

`HotObservable`は、オブザーバがいるいないにかかわらず、指定された時刻に正確にイベントを発行するObservableです。
つまり、オブザーバがサブスクライブするタイミングによってはイベントが受け取れない可能性があるということです。

`ColdObservable`は、新しくオブザーバがサブスクライブすると、イベントを最初からリプレイします。
こちらは`HotObservable`とは違い、オブザーバはサブスクライブするタイミングに関わらず最初のイベントから受け取ることが出来ます。

`HotObservable`と`ColdObservable`については、公式ドキュメントにも記載があります。
https://github.com/ReactiveX/RxSwift/blob/master/Documentation/HotAndColdObservables.md

また、Qiitaでも素晴らしい記事があります。
[RxのHotとColdについて](https://qiita.com/toRisouP/items/f6088963037bfda658d3)

### RxBlocking
RxBlockingは、通常のObservableを`BlockingObservable`に変換します。
`BlockingObservable`は`completed`イベントが発行されるかタイムアウトするまで、カレントスレッドをブロックします。
これにより、イベントが非同期に発行される場合でも簡単にテストをすることができます。

## RxTest、RxBlockingを使ったテストコードの基本パターン
ここからは、RxTestとRxBlockingを使ったテストコードの基本的な書き方のパターンを見ていきます。

なお、テストの初期化コードを以下のように書いておきます。

```swift
class BasicPatterns: QuickSpec {
    override func spec() {
        var scheduler: TestScheduler!
        let disposeBag = DisposeBag()
        
        beforeEach {
            scheduler = TestScheduler(initialClock: 0)
        }
    }
}
```

**[2019/04/23追記]**
**バージョン4.4.0から以下の記述が不要になりました**
https://github.com/ReactiveX/RxSwift/releases/tag/4.4.0

>また、以下のエクステンションを書いておくことで、Nimbleの`expect`でイベントの検証が簡単にできます。

>```swift
>extension Recorded: Equatable where Value: Equatable {}
>extension Event: Equatable where Element: Equatable {}
>```
>
>このエクステンションについてはこちらも参照:
>https://github.com/Quick/Nimble/issues/523

### :fire: HotObservable + start パターン
```swift
it("HotObservable + start") {
    let xs = scheduler.createHotObservable([
        Recorded.next(110, 10),
        Recorded.next(210, 20),
        Recorded.next(310, 30)
    ])

    let res = scheduler.start(created: 100, subscribed: 200, disposed: 1000) {
        xs.map { $0 * 2 }
    }

    expect(res.events).to(equal([
        Recorded.next(210, 40),
        Recorded.next(310, 60)
    ]))
}
```

最初のパターンなので、各文の意味を解説しておきます。

`scheduler.createHotObservable()`メソッドは、`HotObservable`を生成します。
ここでは、110、210、310という時刻に`next`イベントを発行するよう指定しています。
10、20、30というのは、`next`イベントとともにオブザーバに通知される要素(`Element`)です。

`scheduler.start()`メソッドは、テスト対象となるObservableを作成し、作成されたObservableにオブザーバをサブスクライブさせ、最後に破棄するという一連の流れを実行します。
`scheduler.start()`メソッドには、Observable作成を行うクロージャと、Observableの作成、サブスクライブ、破棄それぞれを実行する時刻を指定することが出来ます。

最後に、オブザーバに通知されたイベントの検証を行います。
`res.events`には、オブザーバが受信したイベントの時刻および要素が記録されていますので、想定通りのイベントが通知されているかをここで検証することが出来ます。

今回のテスト対象は`map`オペレーターです。
Observableの作成コードを`xs.map { $0 * 2 }`としているので、オブザーバが受け取る要素はオリジナルの要素の2倍の数値となっているはずです。

検証コードを見ると、時刻210と310のイベントしかありません。
これは、`HotObservable`を使用しているためです。
`HotObservable`はイベントをリプレイしません。
オブザーバーは時刻200にサブスクライブしているため、その前の時刻110に発行されたイベントは受け取れないのです。

以上が`HotObservable + start`パターンです。

> #### scheduler.start()メソッド
> このメソッドのシグネチャは以下のとおりです。
>
> ```swift
> start(created: TestTime, subscribed: TestTime, disposed: TestTime, create: @escaping () -> Observable) -> TestableObserver
> ```
>
> エイリアスとして別に2つのメソッドが定義されています。
>
> ```swift
> start(disposed: TestTime, create: @escaping () -> Observable) -> TestableObserver
>
> start(_ create: @escaping () -> Observable) -> TestableObserver
> ```
>
> `created`、`subscribed`、`disposed`にはそれぞれデフォルト時刻として`100`、`200`、`1000`が設定されているため、先程のコードは以下のように書くことも可能です。
>
> ```swift
> let res = scheduler.start {
>     xs.map { $0 * 2 }
> }
> ```

### :shaved_ice: ColdObservable + start パターン
```swift
it("ColdObservable + start") {
    let xs = scheduler.createColdObservable([
        Recorded.next(110, 10),
        Recorded.next(210, 20),
        Recorded.next(310, 30)
    ])

    let res = scheduler.start(created: 100, subscribed: 200, disposed: 1000) {
        xs.map { $0 * 2 }
    }

    expect(res.events).to(equal([
        Recorded.next(310, 20),
        Recorded.next(410, 40),
        Recorded.next(510, 60)
    ]))
}
```

`HotObservable + start`パターンとの違いは、`scheduler.createColdObservable()`を使っているところです。
こちらの場合、テスト対象となるObservableは`ColdObservable`となります。
`ColdObservable`なので、オブザーバがサブスクライブするタイミングに関わらず、オブザーバはすべてのイベントを受信することができます。
検証コードを見てみると、想定通りすべてのイベントが受信できていることがわかります。

一方で、イベントを受信した時刻は`HotObservable`のときとは少し違います。
`ColdObservable`では、オブザーバがサブスクライブしてからイベントを発行するまでの遅延時間を指定するようです。
今回、オブザーバは時刻200にサブスクライブしており、最初のイベントを発行するまでの遅延時間は110になっているので、オブザーバは時刻310に最初のイベントを受信しています。

以上が`ColdObservable + start`パターンです。

### :fire: HotObservable + scheduleAt + start パターン
```swift
it("HotObservable + scheduleAt + start") {
    let xs = scheduler.createHotObservable([
        Recorded.next(110, 10),
        Recorded.next(210, 20),
        Recorded.next(310, 30)
    ])

    let observer = scheduler.createObserver(Int.self)

    scheduler.scheduleAt(200) {
        xs.map { $0 * 2 }.subscribe(observer).disposed(by: disposeBag)
    }

    scheduler.start()

    expect(observer.events).to(equal([
        Recorded.next(210, 40),
        Recorded.next(310, 60)
    ]))
}
```

やっていることは最初の`HotObservable + start`パターンと同じですが、いくつか新しい関数が出てきました。

`scheduler.createObserver()`は、オブザーバを作成します。
このオブザーバはRxTestが提供する`TestableObserver`というクラスのインスタンスで、Observableが発行したイベントの時刻と要素を記録することができます。
これまでに出てきた変数`res`は、実は`TestableObserver`であり、`scheduler.start()`関数が内部的に作成していたものです。

`scheduler.scheduleAt()`は、指定した時刻に引数として渡されたクロージャを実行します。
ここでは、時刻200にオブザーバをサブスクライブさせています。

`scheduler.start()`は、今までのそれとは違うもので、TestSchedulerが管理している仮想時間を開始する関数です。
これを実行することで、指定時刻にObservableがイベントを発行したり、`scheduler.scheduleAt()`で指定されたクロージャが実行されたりします。

以上が`HotObservable + scheduleAt + start`パターンです。

### :shaved_ice: ColdObservable + scheduleAt + start パターン
```swift
it("ColdObservable + scheduleAt + start") {
    let xs = scheduler.createColdObservable([
        Recorded.next(110, 10),
        Recorded.next(210, 20),
        Recorded.next(310, 30)
    ])

    let observer = scheduler.createObserver(Int.self)

    scheduler.scheduleAt(200) {
        xs.map { $0 * 2 }.subscribe(observer).disposed(by: disposeBag)
    }

    scheduler.start()

    expect(observer.events).to(equal([
        Recorded.next(310, 20),
        Recorded.next(410, 40),
        Recorded.next(510, 60)
    ]))
}
```

`HotObservable + scheduleAt + start`パターンの`ColdObservable`版です。
やっていることは`ColdObservable + start`パターンと同じです。

### :no_entry_sign: Blocking パターン
```swift
it("Blocking") {
    // 非同期にイベントが発行されるObservable
    let observable = Observable.of(10, 20, 30)
        .map { $0 * 2 }
        .observeOn(ConcurrentDispatchQueueScheduler(qos: .background))
    
    let blocking = observable.toBlocking()
    
    expect(try! blocking.first()).to(equal(20))
    expect(try! blocking.last()).to(equal(60))
    expect(try! blocking.toArray()).to(equal([20, 40, 60]))
    expect { try blocking.single() }.to(throwError(RxError.moreThanOneElement))
    
    let materialized = blocking.materialize()
    if case let .completed(elements) = materialized {
        expect(elements).to(equal([20, 40, 60]))
    } else {
        fail("expected completed but got \(materialized)")
    }
}
```

RxBlockingが提供する`toBlocking()`を活用したパターンです。
APIクライアントの関数など、イベントが非同期に発行されるObservableを返す関数をテストするときに有効です。

`toBlocking()`は通常のObservableを`BlockingObservable`に変換します。
`BlockingObservable`は、`completed`イベントが発行されるまでカレントスレッドつまりここではメインスレッドをブロックします。
これにより、非同期に発行されるイベントであっても簡単に要素の検証ができます。

`BlockingObservable`は、要素の検証行うために5つのメソッドを用意しています。

`first()`と`last()`は、Observableが発行した最初と最後の要素を返します。
`toArray()`は、Observableが発行したすべてのイベントの要素を返します。
`single()`は、Observableが１つのイベントだけを発行した場合に、その要素を返します。
イベントが２つ以上発行された場合は例外をスローします。
`materialize()`は、`MaterializedSequenceResult`というEnumを返します。
Observableが`completed`したのか`failed`したのかを検証することができます。

なお、`toBlocking()`を使う場合、`scheduler.createHotObservable()`と`scheduler.createColdObservable()`で生成したObservable(`TestableObservable`)は使用できません。
`TestableObservable`に対して`toBlocking()`を呼ぶと、いつまでたってもテストが終了しません。

```swift
it("Don't do this") {
    let xs = scheduler.createHotObservable([
        Recorded.next(110, 10),
        Recorded.next(210, 20),
        Recorded.next(310, 30),
        Recorded.completed(40)
    ])

    // ブロックされたままになりテストが終了しない
    expect(try! xs.toBlocking().toArray()).to(equal([10, 20, 30]))
}
```

以上が`Blocking`パターンです。

## RxTest、RxBlockingを使ったテストコードの実践パターン
これまでは、RxTest、RxBlockingが提供する機能の使い方の説明がメインであったため、テスト対象は`map`オペレーターを使っただけのシンプルなものを使用してきました。

実際のプロジェクトでは、テスト対象のコードはもう少し複雑になります。
しかし、基本的にはこれまで説明してきた基本パターンを適用していくだけです。

ここからは、実際のプロジェクトで出てくるようなテスト対象のコードに対して、どのようにテストを書いていけばよいのかを説明していきます。

### APIクライアント
HTTP通信を行うAPIクライアントクラスのメソッドの戻り値の型は`Single`を使うことが多いですが、これは`Blocking`パターンを使用できる典型的な例です。
自分は使ったことがないですが、`Completable`や`Maybe`を返す関数も`Blocking`パターンが使えます。

これらのObservableは必ず`completed`か`error`イベントをemitします。
なのでRxBlockingによってこれらのイベントがemitされるまでブロックし、最後に結果を検証する事ができます。

```swift:テスト対象コード
class UserService {
    func fetchUser(by userId: Int) -> Single<User> {
        return Single.create { single in
            Alamofire.request("https://api.example.com/users/\(userId)").responseData { response in
                switch response.result {
                case let .success(data):
                    do {
                        let user = try JSONDecoder().decode(User.self, from: data)
                        single(.success(user))
                    } catch {
                        single(.error(error))
                    }
                case let .failure(error):
                    single(.error(error))
                }
            }
            
            return Disposables.create()
        }
    }
}
```

```swift:テストコード
var userService: UserService!

beforeEach {
    userService = UserService()

    let userJson = "{\"id\": 1, \"name\": \"test-user\"}"
    self.stub(uri("https://api.example.com/users/1"), jsonData(userJson.data(using: .utf8)!))
}

it("should fetch an user") {
    let expectedUser = User(id: 1, name: "test-user")
    expect(try! userService.fetchUser(by: 1).toBlocking().single()).to(equal(expectedUser))
}
```

HTTP通信をスタブ化するために、ここでは[Mockingjay](https://github.com/kylef/Mockingjay)というライブラリを使用しています。

```swift
self.stub(uri("https://api.example.com/users/1"), jsonData(userJson.data(using: .utf8)!))
```

ここでは、HTTPリクエストのURLが`https://api.example.com/users/1`だった場合に`userJson`のJSON文字列をレスポンスとして返すという宣言をしています。
こうすることで、実際にこのURLにはアクセスせずにすぐにレスポンスが帰ってくるようになります。

さて、肝心の`Blocking`パターンによるテストコードですが、ものすごくシンプルになっています。

```swift
expect(try! userService.fetchUser(by: 1).toBlocking().single()).to(equal(expectedUser))
```

ここでテストしているのは、emitされる要素が1つであることと、その要素の値が`expectedUser`と一致していることです。
また、Mockingjayによってリクエストの宛先URLが`https://api.example.com/users/1`であることの検証もしていることになります。

### Subject/Relay
私はViewModelで状態を保持するプロパティの型として`BehaviorRelay`をよく使います。
UITextField等のUIをバインドし、何らかのロジックを実行してUIに結果を反映させるという使い方です。

ここでは`HotObservable + scheduleAt + start`パターンを使用した例をご紹介します。

なお、ここで紹介するテストコードの書き方は、同じRelayの`PublishRelay`や、`PublishSubject`、`BehaviorSubject`などのSubjectでも使えます。

```swift:テスト対象コード
class LoginViewModel {
    let email = BehaviorRelay<String>(value: "")

    var isValidEmail: Driver<Bool> {
        return email.asDriver().map { !$0.isEmpty }
    }

    var isValidPassword: Driver<Bool> {
        return password.asDriver().map { !$0.isEmpty }
    }
    
    var isValidForm: Driver<Bool> {
        return Driver.combineLatest(isValidEmail, isValidPassword).map { $0 && $1 }
    }
}
```

```swift:テストコード
var scheduler: TestScheduler!
var loginViewModel: LoginViewModel!

let disposeBag = DisposeBag()

beforeEach {
    scheduler = TestScheduler(initialClock: 0)
    loginViewModel = LoginViewModel()
}

describe("isValidEmail") {
    it("should be true when email is not empty") {
        let xs = scheduler.createHotObservable([
            Recorded.next(10, ""),
            Recorded.next(20, "a@example.com"),
            Recorded.next(30, "")
        ])

        xs.bind(to: loginViewModel.email).disposed(by: disposeBag)

        let observer = scheduler.createObserver(Bool.self)
        loginViewModel.isValidEmail.drive(observer).disposed(by: disposeBag)

        scheduler.start()

        expect(observer.events).to(equal([
            Recorded.next(0, false),
            Recorded.next(10, false),
            Recorded.next(20, true),
            Recorded.next(30, false)
        ]))
    }
}

describe("isValidForm") {
    it("should be true when both email and password are valid") {
        let xs1 = scheduler.createHotObservable([
            Recorded.next(10, ""),
            Recorded.next(30, "a@example.com"),
            Recorded.next(50, "")
        ])

        let xs2 = scheduler.createHotObservable([
            Recorded.next(20, ""),
            Recorded.next(40, "passw0rd"),
            Recorded.next(60, "")
        ])

        xs1.bind(to: loginViewModel.email).disposed(by: disposeBag)
        xs2.bind(to: loginViewModel.password).disposed(by: disposeBag)

        let observer = scheduler.createObserver(Bool.self)
        loginViewModel.isValidForm.drive(observer).disposed(by: disposeBag)

        scheduler.start()

        expect(observer.events).to(equal([
            Recorded.next(0, false),
            Recorded.next(10, false),
            Recorded.next(20, false),
            Recorded.next(30, false),
            Recorded.next(40, true),
            Recorded.next(50, false),
            Recorded.next(60, false)
        ]))
    }
}
```

#### isValidEmailのテスト
LoginViewModelの`email`プロパティは、ビューのUITextFieldにバインドされることを想定しています。
そして、ビュー側では`isValidEmail`をサブスクライブし、文字が入力されるたびにそのフィールドが有効かどうかをユーザにフィードバックするというイメージです。

テストコードを見てみましょう。
ここでは`email`に対して[`""`, `a@example.com`, `""`]をemitするHotObservableをバインドし、`isValidEmail`は[`false`, `false`, `true`, `false`]をオブザーバに通知することをテストしようとしています。

ここで、時刻0で`false`となっているのは、`BehaviorRelay`はオブザーバがサブスクライブしたときに最後の要素をemitする(リプレイする)という性質があるためです。

#### isValidFormのテスト
`isValidForm`は、`email`と`password`両方が有効な場合のみ`true`を返します。
`isValidForm`はビューの`UIButton.rx.isEnabled`にバインドし、ユーザがログインフォームに文字を入力するたびにフォームが評価され、ボタンがリアクティブに有効になったり無効になったりするというような使い方を想定しています。

テストコードを見てみましょう。
はじめに、2つのHotObservableを作成し、それぞれLoginViewModelの`email`と`password`にバインドします。
次に、オブザーバを作成し、LoginViewModelの`isValidForm`にサブスクライブさせます。
これで準備完了です。

ここで期待しているのは、`email`と`password`両方のフィールドが有効なときのみ`isValidForm`が`true`になることです。
今回の場合、時刻40のときにどちらのフィールドにも文字が1文字以上入力されている状態になるので、時刻40のときに`isValidForm`が`true`になります。

## まとめ
RxTest、RxBlockingの概要、基本的なテストコードパターン、そして実践的なテストコード例を説明しました。

もっと実践的な例をご紹介できればよかったのですが、正直なところ自分が関わっているプロジェクトではこの程度しかテストコードのパターンが出てきませんでした。
特に`ColdObservable`は使ったことがなく、うまい使い方が思いついていないです。

他に想定されるケースとしては、自作オペレータ、3rdパーティライブラリのrxエクステンションなどでしょうか。
今後プロジェクトを進めていく中で新しい実践例を見つけたら、本記事を更新していきたいと思います。
こんなパターンがあるよという方がいましたら、是非コメントいただけると嬉しいです！