CombineLatest Streamのテスト手法

## はじめに
テスト書いてますか？
テストコードが0のアプリにようやくテストコードを入れるようになり、RxTest/RxBlockingを使いはじめました。
`CombineLatest`で複数のObservabelをまとめて観測する値がtuple型のときに、テストで詰まったので解決策を残します。

## 環境
- macOS Catalina 10.15.3
- Xcode 11.3.1 (11C504)
- RxSwift 5.0.1

## Rxのテスト

### Model
テストしたいModelを用意します。
今回は`userStream: Observable<(String, Int)>`の値をテストします。

```swift

import RxSwift
import RxCocoa

class UserModel {

    let name: BehaviorRelay<String> = BehaviorRelay(value: "")
    let age: BehaviorRelay<Int> = BehaviorRelay(value: 0)

    // outputStream
    let userStream: Observable<(String, Int)>

    init(name: String, age: Int) {
        self.name.accept(name)
        self.age.accept(age)

        userStream = Observable.combineLatest(self.name, self.age)
    }
}
```

### テストコード

それではUserModelの`userStream`のテストを書いてみます。

```swift

import XCTest
import RxSwift
import RxCocoa
import RxBlocking
import RxTest
@testable import TestApp

class TestAppTests: XCTestCase {

    var userModel: UserModel!
    var scheduler: TestScheduler!
    var disposeBag: DisposeBag!

    override func setUp() {
        userModel = UserModel(name: "takutoki", age: 5)
        scheduler = TestScheduler(initialClock: 0)
        disposeBag = DisposeBag()
        super.setUp()
    }

    func testObservableWithTuple() {
        // assertする型を定義
        let testObserver = scheduler.createObserver((String, Int).self)

        // テスト対象のbind先をtestObserverにする
        userModel.userStream
            .bind(to: testObserver)
            .disposed(by: disposeBag)

        // name に流すテストObservable
        let nameTestObservable = scheduler.createColdObservable([
            Recorded.next(10, "ShirasakaKoume"),
            Recorded.next(20, "KoshimizuSachiko"),
            Recorded.next(30, "HoshiSyoko")
            ])

        // age に流すテストObservable
        let ageTestObservable = scheduler.createColdObservable([
            Recorded.next(15, 13),
            Recorded.next(25, 14),
            Recorded.next(35, 15)
            ])

        nameTestObservable
            .bind(to: userModel.name)
            .disposed(by: disposeBag)

        ageTestObservable
            .bind(to: userModel.age)
            .disposed(by: disposeBag)
        
        // expect
        // 0: 初期値
        // 10,20,30: nameが変更されたときに流れる
        // 15,25,35: ageが変更されたときに流れる

        let expected: [Recorded<Event<(String, Int)>>] = [
            Recorded.next(0, ("takutoki", 5)),
            Recorded.next(10, ("ShirasakaKoume", 5)),
            Recorded.next(15, ("ShirasakaKoume", 13)),
            Recorded.next(20, ("KoshimizuSachiko", 13)),
            Recorded.next(25, ("KoshimizuSachiko", 14)),
            Recorded.next(30, ("HoshiSyoko", 14)),
            Recorded.next(35, ("HoshiSyoko", 15))
        ]

        scheduler.start()

        XCTAssertEqual(testObserver.events, expected) // Global function 'XCTAssertEqual(_:_:file:line:)' requires that '(String, Int)' conform to 'Equatable'
    }
}
```

XCTAssertEqualの行でコンパイル前にエラーが起きます。

余談ですが、`Xcode10.3`では`ビルド時`に以下のエラーが発生します。
これだと原因がいまいちはっきりしませんね。
実際の直面したときは以下のエラーだったので、解決法がなかなか見つかりませんでした。

```swift
Expression type '()' is ambiguous without more context
```


## どうするか

エラーメッセージにある通り`Equatable`に準拠した型同士でassertするようにすれば良いです。
そこでstruct同士でassertを行うようにします。

```swift
struct AssertStruct: Equatable {
    
    var name: String
    var age: Int
    
    public static func == (lhs: AssertStruct, rhs: AssertStruct) -> Bool {
        return lhs.name == rhs.name && lhs.age == rhs.age
    }
}
```

`Equatable`を準拠した`AssertStruct`を作成して `==`メソッドを実装します。
テストも書き直します。

```swift

func testObservableWithTuple() {
        // assertする型をAssertStructに変更する
        let testObserver = scheduler.createObserver(AssertStruct.self)

        // userStreamをAssertStructに変換するoperatorを追加する
        userModel.userStream
            .map{ AssertStruct(name: $0.0, age: $0.1) }
            .bind(to: testObserver)
            .disposed(by: disposeBag)

        let nameTestObservable = scheduler.createColdObservable([
            Recorded.next(10, "ShirasakaKoume"),
            Recorded.next(20, "KoshimizuSachiko"),
            Recorded.next(30, "HoshiSyoko")
            ])

        let ageTestObservable = scheduler.createColdObservable([
            Recorded.next(15, 13),
            Recorded.next(25, 14),
            Recorded.next(35, 15)
            ])

        nameTestObservable
            .bind(to: userModel.name)
            .disposed(by: disposeBag)

        ageTestObservable
            .bind(to: userModel.age)
            .disposed(by: disposeBag)

        // 期待する値はAssertStructの型に変更する
        let expected: [Recorded<Event<AssertStruct>>] = [
            Recorded.next(0, AssertStruct(name: "takutoki", age: 5)),
            Recorded.next(10, AssertStruct(name: "ShirasakaKoume", age: 5)),
            Recorded.next(15, AssertStruct(name: "ShirasakaKoume", age: 13)),
            Recorded.next(20, AssertStruct(name: "KoshimizuSachiko", age: 13)),
            Recorded.next(25, AssertStruct(name: "KoshimizuSachiko", age: 14)),
            Recorded.next(30, AssertStruct(name: "HoshiSyoko", age: 14)),
            Recorded.next(35, AssertStruct(name: "HoshiSyoko", age: 15))
        ]

        scheduler.start()

        XCTAssertEqual(testObserver.events, expected)
    }
```

<img width="313" alt="スクリーンショット 2020-02-08 15.24.36.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139404/ff4f0ad4-ac31-4b09-73b3-97ff469b7635.png">
All Greeeeen!!!


## 最後に
上記の例では`こんなんテストする必要あるんか？`と思いますが、
実際の業務ではoperatorを複数噛ませたりdistinctUntilChangedなど入れたりと複雑な要件になりがちです。
テストでoutputを素早く確認できるのは安心ですね。