RxTestでスケジューラーを使ったシンプルなサンプル

## はじめに

とにかくRxTestを動かしたいときのためにシンプルサンプルを示したい。

- ViewModel的なものを使う
- TestSchedulerを使う

サンプルは2つ示す。

## シンプルなサンプル1

まずテストしたいViewModelを用意。これの処理自体に意味はないけどとにかくinputとしてタップされた瞬間にもう一つのon/offのイベントに対してoutputが変われば良しというレベルのもの。

- input
    - taps: Signal<()>
        - タップされたイベント的な
    - checks: Signal<Bool> 
        - View側のon/off的な何かがある的な
- output
    - result: Driver<Bool>
        - とりあえずchecksをそのままかえす

```swift
class ViewModel {
    // output: inputによってBoolで変換する
    let result: Driver<Bool>

    init(taps: Signal<()>,        // タップされたイベントがくる
         checks: Signal<Bool>) {  // チェックをon/offした結果がくる
        result = taps.withLatestFrom(checks)
            .flatMapLatest {
                // タップされた際にchecksの値をoutputにするだけ
                return Driver.just($0)
            }
    }
}
```

テストパスするケースを書く

```swift
class ViewModelTests1: XCTestCase {

    let bag = DisposeBag()
    var viewModel: ViewModel!
    var scheduler: TestScheduler!

    override func setUp() {
        scheduler = TestScheduler(initialClock: 0)
    }

    func testExample() {
        let taps = scheduler.createHotObservable([
            Recorded.next(100, ()), // 仮想時刻100にタップ
            Recorded.next(200, ()),
            Recorded.next(300, ())
        ])

        let checks = scheduler.createHotObservable([
            Recorded.next(50, false), // 仮想時刻50にチェックをfalseに
            Recorded.next(150, true),
        ])

        viewModel = ViewModel(taps: taps.asSignal(onErrorJustReturn: ()),
                              checks: checks.asSignal(onErrorJustReturn: false))

        let observer = scheduler.createObserver(Bool.self)
        // 結果をViewModelのoutputからobserverにbindしておく
        viewModel.result
            .drive(observer)
            .disposed(by: bag)

        // 期待値（つまりテストパスするはずのケース）を設定
        let expectedEvents = [
            Recorded.next(100, false), // 100でタップしたときfalseにされてるはず
            Recorded.next(200, true),  // 200でタップしたら150でtrueになってるはず
            Recorded.next(300, true)
        ]

        scheduler.start()
        // observerと期待値を比較
        XCTAssertEqual(observer.events, expectedEvents)
    }
}
```

## シンプルなサンプル2

次はCGFloatのrgbを渡してそれをUIColorへとするViewModelを考えてみる

- input
    - red: Observable<Dobule>
        - 赤色、UI上で0から1の範囲で変更される
- output
    - result: Observable<UIColor>
        - とりあえずUIColorにしてそのままかえす


```swift
    class ViewModel {
        let color: Observable<UIColor>

        init(red: Observable<Double>, green: Observable<Double>, blue: Observable<Double> ) {

            color = Observable.combineLatest(red, green, blue) { r, g, b in
                        print(r, g, b)
                        return UIColor(red: CGFloat(r), green: CGFloat(g), blue: CGFloat(b), alpha: 1.0)
                    }
        }
    }
```

テストパスするケースを書く

```swift
   let bag = DisposeBag()
    // テストしたい対象
    var viewModel: ViewModel!

    // 擬似的な時間を作るヤーツ
    var scheduler: TestScheduler!

    override func setUp() {
        scheduler = TestScheduler(initialClock: 0)
    }

    func testExample() {

        let r = scheduler.createHotObservable([
            Recorded.next(0,   0.0),
            Recorded.next(100, 1.0)
        ])

        let g = scheduler.createHotObservable([
            Recorded.next(0,   0.0),
            Recorded.next(200, 0.5),
        ])

        let b = scheduler.createHotObservable([
            Recorded.next(0, 0.0),
        ])

        let observer = scheduler.createObserver(Int.self)

        viewModel = ViewModel(red: r.asObservable(),
                              green: g.asObservable(),
                              blue: b.asObservable())

        viewModel.color
            .map { $0.hash }
            .subscribe(observer)
            .disposed(by: bag)

        let expectedEvents = [
            Recorded.next(0, UIColor.black.hash),
            Recorded.next(100, UIColor.red.hash),
            Recorded.next(200, UIColor.orange.hash)
        ]

        scheduler.start()

        XCTAssertEqual(observer.events, expectedEvents)
    }
```

UIColorをそのまま比較するというのができなかったため、hashとし実際の比較している値はIntになるが、ViewModelでprintしている結果は次のようになる

```
r:  0.0 g:  0.0 b:  0.0
r:  1.0 g:  0.0 b:  0.0
r:  1.0 g:  0.5 b:  0.0
```

これらが期待値の色のとおりになる。

### 余談

試していて気がついたが仮想時間300にUIColor.purpleにすることを期待してみる

```swift
        let expectedEvents = [
            Recorded.next(0, UIColor.black.hash),
            Recorded.next(100, UIColor.red.hash),
            Recorded.next(200, UIColor.orange.hash),
            Recorded.next(300, UIColor.purple.hash),
        ]
```

purpleは `r: 0.5 g:  0.0 b:  0.5`となればいい。

```swift
        let r = scheduler.createHotObservable([
            Recorded.next(0,   0.0),
            Recorded.next(100, 1.0),
            Recorded.next(300, 0.5)  // 追加
        ])

        let g = scheduler.createHotObservable([
            Recorded.next(0,   0.0),
            Recorded.next(200, 0.5),
            Recorded.next(300, 0.0)  // 追加
        ])

        let b = scheduler.createHotObservable([
            Recorded.next(0, 0.0),
            Recorded.next(300, 0.5)  // 追加
        ])
```

しかし現状のコードではテストは通らない。printを見てみよう

```swift
r:  0.0 g:  0.0 b:  0.0
r:  1.0 g:  0.0 b:  0.0
r:  1.0 g:  0.5 b:  0.0
r:  0.5 g:  0.5 b:  0.0  # 仮想時間300でのイベント
r:  0.5 g:  0.0 b:  0.0  # 仮想時間300でのイベント
r:  0.5 g:  0.0 b:  0.5  # 仮想時間300でのイベント (UIColor.purpleになった)
```

仮想時間300に3つのイベントを作ると、それが順次結果に反映され3つのイベントが増えた。
もちろん何もコードは間違っていないが、複数のイベントに同時の仮想時間を指定したことでそれぞれのイベントが追加された。

こんなテスト書かないとは思うし、実際同時刻にイベントが来ることもないのでどうでもいいものの、どのようにすれば期待通りになるかということを考えると、一つの手としては次のように取り出して順番を変えて比較する方法もある。

```swift
XCTAssertEqual(observer.events[0].value.element, expectedEvents[0].value.element)
XCTAssertEqual(observer.events[1].value.element, expectedEvents[1].value.element)
XCTAssertEqual(observer.events[2].value.element, expectedEvents[2].value.element)
XCTAssertEqual(observer.events[5].value.element, expectedEvents[3].value.element)
```