RxSwiftでのTest事例 

## なぜテストを書くのか？
- コードレビューの効率化
- 密結合から疎結合になり、破壊的な設計を防止
- デバッグ時間の短縮
- バグ & デグレの早期発見 また　発生箇所の特定の短縮
- テームの設計力の向上

### 結論
**開発効率が向上!?**

## Quick, RSpec, Better Specs
[Quickとは](https://github.com/Quick/Quick)
The Swift (and Objective-C) testing framework.
Quick is a behavior-driven development framework for Swift and Objective-C. Inspired by RSpec, Specta, and Ginkgo.
→ Swiftのテストフレームワーク

[RSpecとは](https://github.com/rspec/rspec)
RSpec is a behavior-driven development framework for Ruby.
→ Rubyのテストフレームワーク

[BDDとは](https://behaviourdriven.org/)
Behaviour-Driven Development (BDD) is an evolution in the thinking behind TestDrivenDevelopment and AcceptanceTestDrivenPlanning.
→ テスト駆動開発の派生型（可読性が高い、仕様とテストが同一になりやすい）

[Better Specsとは](http://www.betterspecs.org/jp/)
RSpecで良いテストの書き方を説明したもの

[Nimbleとは](https://github.com/Quick/Nimble)
Nimble is **A Matcher** Framework for Swift and Objective-C

- 自然な英文のようなAPIを提供
- 失敗時のエラーがわかりやすい

Quickでテストコードを構造化して
Nimbleで期待通りの動作をしているかのテストを書きやすく書く！

### 結論
**RSpecに影響されたQuickでテストを書くなら、Better Specsにならって書くことでテストコードの可読性が上がるはず！！**
**Quickでテストコードを構造化して、Nimbleで期待通りの動作をしているかのテストを書きやすく書く！！**

## Quick版 Better Specs
Better Specsについて説明していく！
http://www.betterspecs.org/jp/

*途中で出てくるメソッドやクラスは簡略化して書いているので、動くコードではありません。ニュアンスが伝わる程度です。*

### メソッドの説明をする
作成中のメソッドを明らかにしましょう。

```BAD.swift
describe("the authenticate class") { }

describe("the authenticate method for User") { }
```

```GOOD.swift
describe("Authenticate") { }

describe("authenticate()") { }
```

### Contextsを使う
Contextsはテストを構造化にして、まとめる素晴らしい方法です。
長い目で見ると、この方法はテストを読みやすく、書きやすくなる。

```BAD.swift
it("has 200 status code if logged in") {
    APIStub.add(requestEndpoint)
    expect(user.login.status).to(equal(.success))
}

it("has 401 status code if not logged in") {
    APIStub.add(requestEndpoint, http(401))
    expect(user.login.status).to(equal(.failed))
}
```

```GOOD.swift
context("logged in"){
    context("success") {
        beforEach {
            APIStub.add(requestEndpoint, jsonData(data))
        }
        it("status") {
            expect(user.login.status).to(equal(.success))
        }
    }
    context("faild") {
        beforEach {
             APIStub.add(requestEndpoint, http(401))
        }
        it("status") {
             expect(user.login.status).to(equal(.failed))
        }
    }
}
```

### 説明を短く
specの説明は４０文字を超えないようにしましょう。超えた場合はcontextを分けてください。

```BAD.swift
it("has 422 status code if an unexpected params will be added") { }
```

```GOOD.swift
context("when not valid") {
    it("satus 422") { }
}
```

### 単一条件テスト
`単一条件` は `各テストは一つだけ確認すべき` という表現でより広く知られています。 これはエラーを探しやすくし、失敗するテストをすぐ見つけるようにし、コードを読みやすくします。

独立したユニットでは、各例はただ一つの振る舞いだけテストするのが望ましいです。例の中で多数のテストが有るのは幾つかの振る舞いに分離する必要があることを示しています。

しかし、分離できないテストで（例えばDBや外部システムとの連動、前後があるテストの場合）分離するだけでは同じセットアップを何回も行い、テストが重くなる現象が現れます。こういった重いテストは分けなくても良いでしょう。


```GOOD_ISOLATED.swift
it("first load") {
   expect(results.count).to(equal(10))
}

it("next load") {
   expect(results.count).to(equal(20))
}
```


```GOOD_NOT_ISOLATED.swift
it("first load") { 
   expect(results.events.map { $0.value }).to(equal([.success, .failed, .success]))
   expect(results.events.map { $0.time }).to(equal([10, 20, 30]))
}
```


### 可能な限り全部をテスト
テストはやった方がいいですが、全ケースをテストしないと、有用とはいえません。有効な場合と無効な場合を全部テストしましょう。例えばこんなアクションが有るとしましょう。

```Example.swift
protocol TestModelDataProvider: AnyObject {
    func getTestModel() -> Observable<API.TestModel>
}
```

```BAD.swift
it("getTestModel()") {
    APIStub.add(endpoint, jsonData(data))
    let testModel = try? provider.getTestModel().toBlocking().single()
    expect(testModel).notTo(beNil())
}
```

```GOOD.swift
context("status 200") {
    beforeEach {
        APIStub.add(endpoint, jsonData(data))
    }
    it("getTestModel()") {
        let testModel = try? provider.getTestModel().toBlocking().single()
        expect(testModel).notTo(beNil())
    }
}

context("status 404") {
    beforeEach {
        APIStub.add(endpoint, http(404))
    }
    it("getTestModel()") {
        let testModel = try? provider.getTestModel().toBlocking().single()
        expect(testModel).to(beNil())
    }
}

context("status 500") {
    beforeEach {
        APIStub.add(endpoint, http(500))
    }
    it("getTestModel()") {
        let testModel = try? provider.getTestModel().toBlocking().single()
        expect(testModel).to(beNil())
    }
}
```

### Subjectを使う
もしも、同じsubjectに対して複数のテストをしていたら、subject{}を使ってDRYしましょう。

```GOOD.swift
describe("TestViewModel") {
    var providerMock: TestProviderMock!
    var viewModel: TestViewModel {   // ここがsubjectに相当！！
        return TestViewModel(provider: providerMock)
    }
    context("TestProvider") {
        beforeEach {
            providerMock = HogeTestProviderMock()
        }
        it("hogehoge()") {
            expect(viewModel.hogehoge()).to(equal("hogehoge"))
        }
    }
}
```

### 結論
**BetterSpecにならって書くことで、可読性が高いテストコードが書ける**

## RxSwift Example

### 例題①
```ViewModel.swift
// inputのtextに対して、10文字以内のときにlabelTextを出力する。
class TestViewModel {

    let labelText: Driver<String>
    
    static let maxTextNumber = 10
    static let addText = "Good!"

    init(text: Observable<String>) {
     
        labelText =  text.asDriver(onErrorJustReturn: "")
            .filter { $0.count <= TestViewModel.maxTextNumber }
            .map { $0 + TestViewModel.addText }
    }
}
```

### TestScheduler, TestableObservable, TestableObserver
実時間とは異なる 仮想時間 に基づきイベントを発生させる `RxTest` が提供するClass

- TestScheduler
  - 仮想時間に基づいてイベントを発生させることができるスケジューラ。
  - 仮想時刻を指定して任意アクションを実行
  - TestableObservable TestableObserver のファクトリメソッドを持つ
- TestableObservable
  - 仮想時間にイベントが発生するObservable
- TestableObserver
    - 受信したイベントを仮想時間とともに保持するObserver
- Recorded
    - 生成されたときの、仮想時間と値の記録している
    - TestSchedulerからRecordedを用いてTestableObservableを作成可能
    - TestableObserverには、Recordedが保持されている

### 

### 例題①のTEST１

```TestViewModelSpec.swift
final class TestViewModelSpec: QuickSpec {
    override func spec() {
        describe("TestViewModel") {
            var scheduler: TestScheduler!
            var disposeBag: DisposeBag!
            var inputText: PublishRelay<String>!
            
            var viewModel: TestViewModel! {   // subject
                return TestViewModel(text: inputText.asObservable())
            }
            
            context("output") {
                beforeEach {
                    // タイムを0からスタートしたいため、毎回スケジューラーを生成する。
                    scheduler = TestScheduler(initialClock: 0)
                    // 1つ前のテストで使用している不要な購読を削除する。
                    disposeBag = DisposeBag()
                    
                    // ① schedulerを使って、inputにいつ、どんな値を流すかを決定したTestableObservable<String>を作成して、inputにbindさせる
                    inputText = PublishRelay<String>()
                    scheduler.createColdObservable([.next(10, ""),
                                                    .next(20, "a"),
                                                    .next(30, "abc"),
                                                    .next(40, "abcdefghi"),
                                                    .next(50, "abcdefghigklmn"),
                                                    .next(60, "a")])
                            .bind(to: inputText)
                            .disposed(by: disposeBag)
                }
                
                it("labelText") {
                    // ② viewModel.labelTextの結果を受け取るTestObserverを作成
                    let labelText = scheduler.createObserver(String.self)
                    
                    // viewModel.labelTextに②で作成した、ObserverをBindして結果を監視する
                    viewModel.labelText
                        .drive(labelText)
                        .disposed(by: disposeBag)

                    // --- ここまででlabelTextのテストの用意完了！ ---
                    
                    // スケジューラーを開始させて①で作成したinputに値を流す
                    scheduler.start()
                    
                    // Nimbleを使って値が合っているかをチェック
                    expect(labelText.events).to(equal([
                            .next(10, "GOOD!"),
                            .next(20, "aGOOD!"),
                            .next(30, "abcGOOD!"),
                            .next(40, "abcdefghiGOOD!"),
                            .next(60, "aGOOD!"),
                        ]
                    ))
                }
            }
        }
    }
}
```

**Rxのテストはテストを書くのが結構大変！！😱**
  → Extensionを使って簡単に書けるようにしていこう！！

### 例題①のTEST2
いちいちViewModelのアウトプットとBindしたテスト用のObservableの作成するのがめんどくさい！！

```TestSchedulerExtention.swift
extension TestScheduler {
    
    /// 特定のオブザーバーとバインドしたテストオブザーバーを作成する。
    /// disposeの設定を行う。
    ///
    /// - Parameter source: テストオブザーバーを作成したいObservableConvertibleType
    /// - Returns: sourceの値を返すテストオブザーバー
    func record<O: ObservableConvertibleType>(source: O) -> TestableObserver<O.E> {
        let observer = createObserver(O.E.self)
        let disposable = source.asObservable().bind(to: observer)
        scheduleAt(100000) {
            disposable.dispose()
        }
        return observer
    }
}
```
```TestViewModelSpec.swift
final class TestViewModelSpec: QuickSpec {
    override func spec() {
        describe("TestViewModel") {
            var scheduler: TestScheduler!
            var disposeBag: DisposeBag!
            var inputText: PublishRelay<String>!
            
            var viewModel: TestViewModel! {   // subject
                return TestViewModel(text: inputText.asObservable())
            }
            
            context("output") {
                beforeEach {
                    // タイムを0からスタートしたいため、毎回スケジューラーを生成する。
                    scheduler = TestScheduler(initialClock: 0)
                    // 1つ前のテストで使用している不要な購読を削除する。
                    disposeBag = DisposeBag()
                    
                    // ① schedulerを使って、inputにいつ、どんな値を流すかを決定したObservableを作って、inputにbindさせる
                    inputText = PublishRelay<String>()
                    scheduler.createColdObservable([.next(10, ""),
                                                    .next(20, "a"),
                                                    .next(30, "abc"),
                                                    .next(40, "abcdefghi"),
                                                    .next(50, "abcdefghigklmn"),
                                                    .next(60, "a")])
                            .bind(to: inputText)
                            .disposed(by: disposeBag)
                }
                
                it("labelText") {
                    // 特定のオブザーバーとバインドしたTestObserverを作成する。
                    let labelText = scheduler.record(source: viewModel.labelText)

                    // --- ここまででlabelTextのテストの用意完了！ ---
                    
                    // スケジューラーを開始させて①で作成したinputに値を流す
                    scheduler.start()
                    
                    // Nimbleを使って値が合っているかをチェック
                    expect(labelText.events).to(equal([
                            .next(10, "GOOD!"),
                            .next(20, "aGOOD!"),
                            .next(30, "abcGOOD!"),
                            .next(40, "abcdefghiGOOD!"),
                            .next(60, "aGOOD!"),
                        ]
                    ))
                }
            }
        }
    }
}
```

結果を受け取るTestObserverの作成が１行で書けるようになった！！

### 例題①のTEST3
input用とexpectでの検証がめんどくさい！！

```TestSchedulerExtention.swift
extension TestScheduler {
    func parseEventsAndTimes<T>(timeline: String, values: [String: T], errors: [String: Swift.Error] = [:]) -> [[Recorded<Event<T>>]] {
        //print("parsing: \(timeline)")
        typealias RecordedEvent = Recorded<Event<T>>
        
        let timelines = timeline.components(separatedBy: "|")
        
        let allExceptLast = timelines[0 ..< timelines.count - 1]
        
        return (allExceptLast.map { $0 + "|" } + [timelines.last!])
            .filter { $0.count > 0 }
            .map { timeline -> [Recorded<Event<T>>] in
                let segments = timeline.components(separatedBy:"-")
                let (time: _, events: events) = segments.reduce((time: 0, events: [RecordedEvent]())) { state, event in
                    let tickIncrement = event.count + 1
                    
                    if event.count == 0 {
                        return (state.time + tickIncrement, state.events)
                    }
                    
                    if event == "#" {
                        let errorEvent = RecordedEvent(time: state.time, value: Event<T>.error(NSError(domain: "Any error domain", code: -1, userInfo: nil)))
                        return (state.time + tickIncrement, state.events + [errorEvent])
                    }
                    
                    if event == "|" {
                        let completed = RecordedEvent(time: state.time, value: Event<T>.completed)
                        return (state.time + tickIncrement, state.events + [completed])
                    }
                    
                    guard let next = values[event] else {
                        guard let error = errors[event] else {
                            fatalError("Value with key \(event) not registered as value:\n\(values)\nor error:\n\(errors)")
                        }
                        
                        let nextEvent = RecordedEvent(time: state.time, value: Event<T>.error(error))
                        return (state.time + tickIncrement, state.events + [nextEvent])
                    }
                    
                    let nextEvent = RecordedEvent(time: state.time, value: Event<T>.next(next))
                    return (state.time + tickIncrement, state.events + [nextEvent])
                }
                
                //print("parsed: \(events)")
                return events
        }
    }
}
```
```TestViewModelSpec.swift
final class TestViewModelSpec: QuickSpec {
    override func spec() {
        describe("TestViewModel") {
            var scheduler: TestScheduler!
            var disposeBag: DisposeBag!
            
            var inputText: [Recorded<Event<String>>]!
            
            var viewModel: TestViewModel! {   // subject
                return TestViewModel(text: scheduler.createHotObservable(inputText).asObservable())
            }

            context("output") {
                
                var expectOutputText: [Recorded<Event<String>>]!

                let inputString = [
                    "s1" : "a",
                    "s2" : "abc",
                    "s3" : "abcdefghigklmn",
                    "e" : ""
                ]

                let expectString = [
                    "e1" : "a" + TestViewModel.addText,
                    "e2" : "abc" + TestViewModel.addText,
                    "e3" : "" + TestViewModel.addText
                ]
                
                beforeEach {
                    scheduler = TestScheduler(initialClock: 0)
                    disposeBag = DisposeBag()

                    (
                        inputText,
                        expectOutputText
                    ) =
                    (
                        scheduler.parseEventsAndTimes(timeline: "e---s1---s2---s3---s1---", values: inputString).first!,
                        scheduler.parseEventsAndTimes(timeline: "e3--e1---e2--------e1---", values: expectString).first!
                    )
                }
                
                it("labelText") {
                    let labelText = scheduler.record(source: viewModel.labelText)
                    
                    scheduler.start()
                    
                    expect(labelText.events).to(equal(expectOutputText))
                }
            }
        }
    }
}
```
inputを流すTestObservableが直感的にかけ、可読性が向上した！！

### 例題②
outputが増えたとき！！

```TestViewModel.swift
// inputのtextに対して、labelTextとerrorTextを出力する
// eroorは10文字以上入力されたときに、alertを出力する
class TestViewModel {
    
    let labelText: Driver<String>
    let alertTrigger: Driver<Void>
    
    static let maxTextNumber = 10
    static let addText = "Good!"

    init(text: Observable<String>) {
        
        let textDriver = text.asDriver(onErrorJustReturn: "")
        
        labelText = textDriver
            .filter { $0.count <= TestViewModel.maxTextNumber }
            .map { $0 + TestViewModel.addText }
        
        alertTrigger = textDriver
            .filter { $0.count > TestViewModel.maxTextNumber }
            .map { _ in () }
    }
}
```

### 例題② TEST
```TestViewModelSpec.swift
final class TestViewModelSpec: QuickSpec {
    override func spec() {
        describe("TestViewModel") {
            var scheduler: TestScheduler!
            var disposeBag: DisposeBag!
            
            var inputText: [Recorded<Event<String>>]!
            
            var viewModel: TestViewModel! {   // subject
                return TestViewModel(text: scheduler.createHotObservable(inputText).asObservable())
            }
            
            context("output") {
                
                var expectOutputText: [Recorded<Event<String>>]!
                var expectOutputAlert: [Recorded<Event<Void>>]!
                
                let inputString = [
                    "s1" : "a",
                    "s2" : "abc",
                    "s3" : "abcdefghigklmn",
                    "e" : ""
                ]
                
                let expectString = [
                    "e1" : "a" + TestViewModel.addText,
                    "e2" : "abc" + TestViewModel.addText,
                    "e3" : "" + TestViewModel.addText
                ]
                
                let expectAlert = [
                    "a" : ()
                ]
                
                beforeEach {
                    scheduler = TestScheduler(initialClock: 0)
                    disposeBag = DisposeBag()
                    
                    (
                        inputText,
                        expectOutputText,
                        expectOutputAlert
                        ) =
                        (
                            scheduler.parseEventsAndTimes(timeline: "e---s1---s2---s3---s1---", values: inputString).first!,
                            scheduler.parseEventsAndTimes(timeline: "e3--e1---e2--------e1---", values: expectString).first!,
                            scheduler.parseEventsAndTimes(timeline: "--------------a---------", values: expectAlert).first!
                    )
                }
                
                it("labelText") {
                    let labelText = scheduler.record(source: viewModel.labelText)
                    
                    scheduler.start()
                    
                    expect(labelText.events).to(equal(expectOutputText))
                }
                
                it("alertTrigger") {
                    let alertTrigger = scheduler.record(source: viewModel.alertTrigger)
                    
                    scheduler.start()
                    
                    expect(alertTrigger.events.map { $0.time }).to(equal(expectOutputAlert.map { $0.time }))
                }
            }
        }
    }
}
```

### 結論
**Quickで構造化することと、Extensionを使うことでRx周りのテストを書きやすくする！！**

## 参考
https://github.com/ReactiveX/RxSwift/blob/master/Documentation/UnitTests.md
https://github.com/ReactiveX/RxSwift/blob/master/RxExample/RxExample-iOSTests/RxExample_iOSTests.swift
https://speakerdeck.com/yusukehosonuma/quick-nimble-more-useful
https://qiita.com/tasanobu/items/6672d6d4d1024d97c82a
https://speakerdeck.com/rockname/rspec-like-test-in-swift
https://qiita.com/Kuniwak/items/4314451227f4d5eaa6b8