SwiftのRxSwiftでのテストコードをRxBlockingとRxTestを使って導入するチュートリアル [iOS]

概要

RxSwiftでのテストの書き方が分からなかったので海外の記事を参考にすることにしました。
これまで色々なiOSのアプリの開発・運用に携わってきましたが運が悪かったのかテストコードが1行も書かれていないプロジェクトばかりを担当してきました。
もちろん、それはiOSエンジニアのスキルが低いから、というわけではなくそもそもiOSアプリ開発に置いて今の所、テストのベストプラクティスが確立されていないからだと思います。
そのため、

「国内」ではなく「海外」ではどうなのか?

という好奇心から色々と記事を探してみました。

こちらは下記のページの翻訳です。

Testing Your RxSwift Code

余談ですが、先月の技術書典では数少ないiOSの「テストコードの書き方」に焦点を当てた技術書がありましたので参考書として紹介しようと思います。

さわって学べる!iOSテスト駆動開発

ただし、こちらはUIテストについては書かれていませんのでご注意ください。
と言っても、私はいまだにまともなUITest本だったりベストプラクティスを見つけた事がありません。
ご存知の方はコメント欄で紹介して頂けると嬉しいです。

英語本なら翻訳もします。
それぐらいテスト系の技術書が欲しいです。

という事で本編に移ります。

前置き

RxSwiftを使って反応的なアプリを書くことは、アプリを「普通の方法」で書くこととは概念的に異なる作業です。アプリ内のものは通常は単なる値ではなく、代わりに値のストリームとして表されます。RxSwiftライブラリでObservableとして知られています。 このチュートリアルでは、RxSwiftコードをテストするための鍵を教えています。

ストリームは、デベロッパーとしての変更に対応して、アプリが常に更新されるようにする強力なメカニズムです。 これが提供する利点の多くは、値のストリームをテストすることは、単純に単一の値をアサートすることほど簡単ではありません。 しかし、心配しないで! - このチュートリアルでは、RxSwiftテストのエキスパートになる方法をご紹介します!

このチュートリアルでは、Observableストリームの単体テストを作成する方法を説明します。 RxSwiftコードをテストするためのテクニックや、ヒントやヒントをいくつか学びます。 始めましょう。

このチュートリアルでは、RxSwiftの使用方法やXCTestを使用した基本的なテストの作成方法に精通していることを前提としています。  
RxSwiftを使って反応的なアプリケーションを構築する方法の詳細については、「RxSwift:Reactive Programming with Swift」を参照してください。

Getting Started

変化するコンテンツを扱うときは反応的なアプリケーションが本当に輝いているので、もちろんその性質のアプリをテストすることになるでしょう!

このチュートリアルの上部または下部にある「素材をダウンロード」ボタンを使用します。 このチュートリアルでは、あなたの音楽の正確さを実践するために使用できる楽しいメトロノームアプリであるRaytronomeのスタータープロジェクトがあります。 あなたがイメージできるように、メトロノームは時間を扱うので、ここでテストするための興味深いロジックと情報がたくさんあります。

Raytronome.xcworkspaceを開きます。 その後Main.storyboardを開きます。 1つの画面しか持たない非常にシンプルなアプリだとわかるでしょう。

アプリをビルドして実行します。 再生ボタンをタップしてメトロノームを開始します。 拍子やテンポを変更することもできます。

test1.gif

このアプリケーションは、単一のView Controller(MetronomeViewController.swift)とMetronomeViewModel.swiftで構成され、すべてのビジネスロジックが含まれています。

The Challenges of Testing Streams

RxSwiftとObservableストリームの基本を簡単に要約します。

ストリームの操作は、基本的な値やオブジェクトを扱う場合と本質的に異なります。 したがって、それらをテストするタスクも異なります。

値は単体で独立しています。それらは時間の表現や概念を持っていません。 一方で観測可能なストリームは、時間の経過と共に要素(例えば、値)を放出します。

test2.png

つまり値のストリームをテストするときは、次のいずれかをテストする必要があります。

  • 一部のストリームは、時間に関係なく特定の要素を出力します。
  • 一部のストリームは、特定の時刻に特定の要素を出力します。 この場合、放出された要素をストリームが放出したときと一緒に「記録する」方法が必要になります。

Determining What to Test

あなたが実際にテストしたいことについて考えてみるのは、しばらく時間をとることをお勧めします。

前述のように、メトロノームに関連する実際のビジネスロジックを含むビューモデルであるMetronomeViewModelをテストします。

MetronomeViewModel.swiftを開きます。 ビューモデルを見ると、分子、分母、シグネチャ、テンポストリング、分子の実際の値、分子の最大値、ビートの原因となるストリームなど、いくつかのロジックを担当する出力を見ることができます。

このアプリはDriverのインスタンスを使用してすべての出力を表します。 ドライバは、UIコンポーネントを扱うときにあなたの人生を楽にする種類のストリームです。

test3.png

あなたがUIでテストしたいものについて考えてみましょう。クイックリストを作成します。
あなたはそれをテストしたい。

  • numeratorとdenominatorは44から始まります。
  • signatureは4/4から始まります。
  • tempoは120から始まります。
  • Play/Pauseボタンをタップすると、metronomeのisPlayingの状態が変わります。
  • numerator、denominatorまたはtempoを変更することで、適切なテキスト表現が生成されます。
  • beatはsignatureに合わせて「beating」します。
  • beatは.even.oddの間で交互に表示されます。アプリはこれを使用して、ビューの上部にあるmetronomeのイメージを設定します。

テストを書くときは、RxSwiftにバンドルされているRxBlockingRxTestという2つの追加のフレームワークを使用します。 それぞれは、ストリームをテストするためのさまざまな機能と概念を提供します。 これらのフレームワークは、すでにあなたのスタータープロジェクトの一部です。

Using RxBlocking

スタータープロジェクトには、RaytronomeTests.swiftファイルを含むbare-bonesのテストターゲットが含まれています。

それを開いて見てください。 RxSwiftRxCocoaRxTestRxBlockingをインポートし、viewModelプロパティと基本的なsetUp()メソッドを組み込んで、すべてのテストケースの前にビューモデルの新しいインスタンスMetronomeViewModelを作成します。

最初のテストケースは、numeratorとdenominator の両方が4の値で始まることを確認することになります。つまり、これらの各ストリームの最初に放出された値のみに注意します。 RxBlockingの完璧な仕事のように思えます!

RxBlockingは、RxSwiftで利用可能な2つのテストフレームワークの1つで、ObservableストリームをBlocking Observableに変換することができます。Blocking Observableは、現在のスレッドをブロックし、オペレータによって指定された特定の条件を待つ特別な観測です。

test4.jpg

終了シーケンス(つまり、completedイベントまたはerrorイベントを発行(emit)するイベント)を処理している状況、または有限数のイベントをテストすることを目的としている場合に役立ちます。

RxBlockingはいくつかの演算子を提供しますが、最も有用なものは...

  • toArray():シーケンスが終了するのを待ち、すべての結果を配列として返します。
  • first():最初の要素を待ち、それを返します。
  • last():シーケンスが終了するのを待ち、放出された最後のアイテムを返します。

これらの演算子を見ると、first()がこの特定のケースに最も適しています。

次の2つのテストケースをRaytronomeTestsクラスに追加します。

RaytronomeTests.swift
func testNumeratorStartsAt4() throws {
  XCTAssertEqual(try viewModel.numeratorText.toBlocking().first(), "4")
  XCTAssertEqual(try viewModel.numeratorValue.toBlocking().first(), 4)
}

func testDenominatorStartsAt4() throws {
  XCTAssertEqual(try viewModel.denominatorText.toBlocking().first(), "4")
}

あなたは通常のストリームをBlockingObservableに変換するためにtoBlocking()を使い、first()を使って最初に送出された要素を待って返します。 あなたは他の定期的なテストのように、それに対して断言することができます。

テストメソッドには、RxBlockingのoperatorがスローする可能性があるため、signaturesにthrowsが含まれていることに注意してください。 テストメソッド自体にthrowを付けると、try!を回避するのに便利です。 内部的に例外がスローされた場合、正常にテストに失敗することがあります。

Command-Uを押してテストを実行します。

test5.png

簡単な挑戦として、次の2つのテストを試してみて、signatureText4/4として開始し、tempoText120 BPMとして開始することを確認してください。テストは上記の2つのテストとほぼ同じである必要があります。

作業が終わったら、テストスイート全体をもう一度実行し、4回の合格テストに合格することを確認してください。

あなたが立ち往生する場合は、Revealボタンをタップして解決策を覗いてみてください。

Reveal

Advantages and Disadvantages of RxBlocking

ご存じのように、RxBlockingは非常に優れており、非常によく知られているコンストラクトの下で反応的な概念を「包み込む」ようになっています。 残念ながら、それには注意すべきいくつかの制限があります。

  1. これは、有限シーケンスをテストすることを目的としています。つまり、完成したシーケンスの最初の要素または要素のリストをテストする場合、RxBlockingは非常に便利です。 しかし、非終端シーケンスを処理するより一般的なケースでは、RxBlockingを使用しても必要な柔軟性は得られません。
  2. RxBlockingは、現在のスレッドをブロックし、実際に実行ループをロックすることによって機能します。 Observableが比較的長い間隔または遅延でイベントをスケジュールする場合、BlockingObservableは同期問題のイベントを待機します。
  3. タイムベースのイベントをアサートし、正しいタイムスタンプが含まれていることを確認するのに興味があるときは、RxBlockingは時間だけでなく要素をキャプチャするだけなので、何の助けにもなりません。
  4. 非同期入力に依存する出力をテストする場合、RxBlockingは、現在のスレッドをブロックするときに有用ではありません。例えば、他の観測可能なトリガを必要とする出力をテストする場合です。

実装する必要がある次のテストは、これらの制限のほとんどに対応しています。 例:Play/Pauseボタンをタップすると、isPlaying出力が新たに発生します。これには、非同期トリガー(tappedPlayPauseinput)が必要です。 また、排出量をテストすることも有益であろう。

Using RxTest

最後のセクションで述べたように、RxBlockingは大きな利点を提供しますが、ストリームのイベント、時間、他の非同期トリガーとの関係を徹底的にテストすることには少し欠けているかもしれません。

これらの問題のすべてを解決するために、RxTestが救助に来ます!

RxTestRxBlockingとはまったく異なるビーストですが、主な違いは能力とストリームに関する情報の方がはるかに柔軟であることです。 これは、TestSchedulerという独自の特別なスケジューラーを提供するため、これを行うことができます。

test6.jpg

コードに入る前に、schedulerが実際に何をしているのか検討する価値があります。

Understanding Schedulers

schedulerは、RxSwiftの下位概念のビットですが、テストでの役割をよりよく理解するためには、schedulerが何であり、どのように機能しているかを理解することが重要です。

RxSwiftはschedulerを使用して、作業をどのように実行するかを抽象的に記述し、その作業の結果として発生するイベントをスケジュールします。

なぜこれが面白いですか、とあなたは思うかもしれません。

RxTestは、TestSchedulerというカスタムスケジューラをテスト専用に提供しています。ObservableObserversを作成して、これらのイベントを「record」してテストできるようにすることで、時間ベースのイベントを簡単にテストできます。

schedulersの詳細を知りたい場合は、公式のドキュメントにいくつかの洞察とガイドラインがあります。

Writing Your Time-Based Tests

テストを書く前に、TestSchedulerインスタンスを作成する必要があります。 また、DisposeBagをクラスに追加して、テストで作成されるDisposablesを管理します。 viewModelプロパティの下に、次のプロパティを追加します。

RaytronomeTests.swift
var scheduler: TestScheduler!
var disposeBag: DisposeBag!

次に、setUp()の最後に次の行を追加して、すべてのテストの前に新しいTestSchedulerおよびDisposeBagを作成します。

RaytronomeTests.swift
scheduler = TestScheduler(initialClock: 0)
disposeBag = DisposeBag()

TestSchedulerの初期化は、ストリームの「開始時刻」を定義するinitialClock引数を取ります。 新しいDisposeBagは、以前のテストで残ったサブスクリプションを取り除いてくれます。

実際のtest writingに向かいましょう!

最初のテストでは、Play/Pauseボタンが数回トリガーされ、isPlaying出力が変更に応じて出力されることをアサートします。

これを行うには、次のことが必要です。

  1. tappedPlayPause入力に偽の「タップ」を発するmockのObservableなストリームを作成します。
  2. Observerライクな型を作成して、isPlayingの出力から放出されたイベントを記録します。
  3. 記録されたイベントはあなたが期待するイベントであると主張する。

これはたくさんのように見えるかもしれませんが、いかにして一緒になるのかを見ると驚くでしょう!
いくつかの事例を例に挙げて説明します。最初のRxTestベースのテストを追加します。

RaytronomeTests.swift
func testTappedPlayPauseChangesIsPlaying() {
  // 1
  let isPlaying = scheduler.createObserver(Bool.self)

  // 2
  viewModel.isPlaying
    .drive(isPlaying)
    .disposed(by: disposeBag)

  // 3
  scheduler.createColdObservable([.next(10, ()),
                                  .next(20, ()),
                                  .next(30, ())])
           .bind(to: viewModel.tappedPlayPause)
           .disposed(by: disposeBag)

  // 4
  scheduler.start()

  // 5
  XCTAssertEqual(isPlaying.events, [
    .next(0, false),
    .next(10, true),
    .next(20, false),
    .next(30, true)
  ])
}

これは少し威圧している場合は心配しないでください。 それを壊します。

  1. TestSchedulerを使用して、mockにしたい要素の型のTestableObserverを作成します。この場合、Boolです。 この特別なobserverの主な利点の1つは、追加されたイベントをアサートするために使用できるeventsプロパティを公開することです。
  2. isPlayingの出力を新しいTestableObserverdrive()します。 ここでイベントを「record」します。
  3. tappedPlayPause入力への3つの「taps」の放出を模倣するモックObservableを作成します。 ここでも、これは、TestableObservableという特殊な型のObservableで、TestSchedulerを使用して、指定された仮想時間にイベントを発生させます。
  4. テストスケジューラでstart()を呼び出します。 このメソッドは、以前のポイントで作成されたpendingのサブスクリプションをトリガーします。
  5. RxTestにバンドルされたXCTAssertEqualの特別なoverloadを使用すると、isPlayingのイベントを両方の要素と時間で予想されるものと同等にすることができます。 10,20および30は、入力が発生した時刻に対応し、0isPlayingの最初の出力です。

混乱するって?このように考えてみましょう。イベントのストリームを「模擬」し、特定の時刻にビューモデルの入力にフィードします。 次に、適切なタイミングで予想されるイベントが出力されるように出力をアサートします。

test7.png

Command-Uを押して、もう一度テストを実行します。 5つの合格テストがあるはずです。

test9.png

Understanding Time Values

おそらく、0102030の値が時間に使用されていることに気がつき、これらの値が実際に意味するものが何なのか不思議だと思うでしょう。 それらが実際の時間とどのように関係していますか?

RxTestは、定期的な時刻(Dateなど)をVirtualTimeUnitIntで表される)に変換する内部メカニズムを使用します。

RxTestでイベントをスケジューリングする場合、使用する時間はあなたが望むもので何でもかまいません。それらは完全に任意であり、TestSchedulerは他のスケジューラのようにイベントをスケジューリングするためにそれらを使用します。

この仮想時間は実際の秒と実際には対応していません。つまり、10は実際には10秒を意味するのではなく、仮想時間のみを表します。 このチュートリアルの後半で、このメカニズムの内部についてもう少し詳しく学びます。

TestSchedulerの時間について深く理解したので、あなたのビューモデルにさらにテストカバレッジを追加するのはどうでしょうか?

前のテストの直後に次の3つのテストを追加します。

RaytronomeTests.swift
func testModifyingNumeratorUpdatesNumeratorText() {
  let numerator = scheduler.createObserver(String.self)

  viewModel.numeratorText
           .drive(numerator)
           .disposed(by: disposeBag)

  scheduler.createColdObservable([.next(10, 3),
                                  .next(15, 1)])
           .bind(to: viewModel.steppedNumerator)
           .disposed(by: disposeBag)

  scheduler.start()

  XCTAssertEqual(numerator.events, [
    .next(0, "4"),
    .next(10, "3"),
    .next(15, "1")
  ])
}

func testModifyingDenominatorUpdatesNumeratorText() {
  let denominator = scheduler.createObserver(String.self)

  viewModel.denominatorText
           .drive(denominator)
           .disposed(by: disposeBag)

  // Denominator is 2 to the power of `steppedDenominator + 1`.
  // f(1, 2, 3, 4) = 4, 8, 16, 32
  scheduler.createColdObservable([.next(10, 2),
                                  .next(15, 4),
                                  .next(20, 3),
                                  .next(25, 1)])
          .bind(to: viewModel.steppedDenominator)
          .disposed(by: disposeBag)

  scheduler.start()

  XCTAssertEqual(denominator.events, [
    .next(0, "4"),
    .next(10, "8"),
    .next(15, "32"),
    .next(20, "16"),
    .next(25, "4")
  ])
}

func testModifyingTempoUpdatesTempoText() {
  let tempo = scheduler.createObserver(String.self)

  viewModel.tempoText
           .drive(tempo)
           .disposed(by: disposeBag)

  scheduler.createColdObservable([.next(10, 75),
                                  .next(15, 90),
                                  .next(20, 180),
                                  .next(25, 60)])
           .bind(to: viewModel.tempo)
           .disposed(by: disposeBag)

  scheduler.start()

  XCTAssertEqual(tempo.events, [
    .next(0, "120 BPM"),
    .next(10, "75 BPM"),
    .next(15, "90 BPM"),
    .next(20, "180 BPM"),
    .next(25, "60 BPM")
  ])
}

これらのテストでは以下のことが行われます

  • testModifyingNumeratorUpdatesNumeratorText:numeratorを変更すると、テキストが正しく更新されることをテストします。
  • testModifyingDenominatorUpdatesNumeratorText:denominatorを変更すると、テキストが正しく更新されることをテストします。
  • testModifyingTempoUpdatesTempoText:tempoを変更すると、テキストが正しく更新されることをテストします。

うまくいけば、前のテストと非常に似ているので、このコードで自宅にいるように感じるでしょう。 numeratorを3に変えてから1に変えてください。そして、numeratorText"4"(4/4のsignatureの初期値)、 "3"、そして最終的に "1"を出していると主張します。

同様に、denominatorの値を変更するとdenominatorTextも更新されることをテストします。 実際の表示は4,8,16、および32ですが、numeratorは実際には1~4です。

最後に、tempoを正しく更新すると、BPMsuffix付きの文字列表現が適切に出力されると主張します。

Command-Uを押してテストを実行し、合計8回の合格テストを残します。 素敵ですね!

test12.png

OK - あなたはそれのコツを得たように思えます!

それを一歩上げる時です。 次のテストを追加します。

RaytronomeTests.swift
func testModifyingSignatureUpdatesSignatureText() {
  // 1
  let signature = scheduler.createObserver(String.self)

  viewModel.signatureText
           .drive(signature)
           .disposed(by: disposeBag)

  // 2
  scheduler.createColdObservable([.next(5, 3),
                                  .next(10, 1),

                                  .next(20, 5),
                                  .next(25, 7),

                                  .next(35, 12),

                                  .next(45, 24),
                                  .next(50, 32)
                                ])
           .bind(to: viewModel.steppedNumerator)
           .disposed(by: disposeBag)

  // Denominator is 2 to the power of `steppedDenominator + 1`.
  // f(1, 2, 3, 4) = 4, 8, 16, 32
  scheduler.createColdObservable([.next(15, 2), // switch to 8ths
                                  .next(30, 3), // switch to 16ths
                                  .next(40, 4)  // switch to 32nds
                                ])
           .bind(to: viewModel.steppedDenominator)
           .disposed(by: disposeBag)

  // 3
  scheduler.start()

  // 4
  XCTAssertEqual(signature.events, [
    .next(0, "4/4"),
    .next(5, "3/4"),
    .next(10, "1/4"),

    .next(15, "1/8"),
    .next(20, "5/8"),
    .next(25, "7/8"),

    .next(30, "7/16"),
    .next(35, "12/16"),

    .next(40, "12/32"),
    .next(45, "24/32"),
    .next(50, "32/32")
  ])
}

深呼吸しましょう! これは本当に新しいものか恐ろしいものではありませんが、あなたがこれまでに書いた同じテストのものより長いバリエーションです。 stepppedNumeratorsteppedDenominatorの両方の入力に要素を連続して追加してすべての種類の異なる拍子記号を作成すると、signatureText出力が適切に書式設定されたsignatureを出力することをアサーションします。

テストをより視覚的に見ると、これはより明確になります。

test13.png

あなたのテストスイートを自由に実行してください。 今は9回の合格テストがあります!

次に、より複雑なユースケースに亀裂が生じます。

次のシナリオを考えてみましょう。

  1. アプリは4/4のsignatureで始まります。
  2. あなたは24/32のsignatureに切り替えます。
  3. 次に、denominatorの - ボタンを押します。 24/16、24/8、および24/4がメトロノームに有効なmeterではないため、signatureが16/16に、次に8/8に、そして最終的に4/4に落ちるはずです。
これらのmeterのいくつかは音楽的に有効ですが、あなたはメトロノームのためにそれらを違法とみなします。

このシナリオのテストを追加します。

RaytronomeTests.swift
func testModifyingDenominatorUpdatesNumeratorValueIfExceedsMaximum() {
  // 1
  let numerator = scheduler.createObserver(Double.self)

  viewModel.numeratorValue
           .drive(numerator)
           .disposed(by: disposeBag)

  // 2

  // Denominator is 2 to the power of `steppedDenominator + 1`.
  // f(1, 2, 3, 4) = 4, 8, 16, 32
  scheduler.createColdObservable([
      .next(5, 4), // switch to 32nds
      .next(15, 3), // switch to 16ths
      .next(20, 2), // switch to 8ths
      .next(25, 1)  // switch to 4ths
      ])
      .bind(to: viewModel.steppedDenominator)
      .disposed(by: disposeBag)

  scheduler.createColdObservable([.next(10, 24)])
           .bind(to: viewModel.steppedNumerator)
           .disposed(by: disposeBag)

  // 3
  scheduler.start()

  // 4
  XCTAssertEqual(numerator.events, [
    .next(0, 4), // Expected to be 4/4
    .next(10, 24), // Expected to be 24/32
    .next(15, 16), // Expected to be 16/16
    .next(20, 8), // Expected to be 8/8
    .next(25, 4) // Expected to be 4/4
  ])
}

ちょっと複雑ですが、何も処理できません! それを分割します。

  1. いつものように、まずTestableObserverを作成し、numeratorValueの出力をそれに送ります。
  2. ここでは、物事は少し混乱しますが、下の視覚的表現を見ると、それがより明確になります。 32のdenominatorに切り替えることから始まり、次に24分の1のnumeratorに切り替えて、24/32メートルにします。 次に、denominatorを段階的にドロップして、モデルがnumeratorValue出力の変更を放出するようにします。
  3. schedulerを開始します。
  4. 適切なnumeratorValueが各ステップごとに発行されると主張します。

test14.png

あなたが作ったかなり複雑なテストです。 Command-Uを押してテストを実行しましょう。

XCTAssertEqual failed: ("[next(4.0) @ 0, next(24.0) @ 10]") is not equal to ("[next(4.0) @ 0, next(24.0) @ 10, next(16.0) @ 15, next(8.0) @ 20, next(4.0) @ 25]") -

あらいやだ! テストは失敗しました。

期待される結果を見ると、denominatorが下がっても、numeratorValueの出力は24のままで、24/16や24/4などの違法なsignaturesが残っているようです。 アプリケーションをビルドして実行し、自分で試してみてください:

  • denominatorを増やして、あなたを4/8のsignatureにしてください。
  • numeratorにも同じことをして、7/8のsignatureにする。
  • denominatorを1つ落としてください。 4/4になるはずですが、実際には7/4になっています - メトロノームの違法なsignatureです!

test15.gif

あなたがバグを見つけたようです。

もちろん、それを修正する責任を負う選択をします。

MetronomeViewModel.swiftを開き、numeratorValueの設定を担当する次のコードを見つけます。

MetronomeViewModel.swift
numeratorValue = steppedNumerator
  .distinctUntilChanged()
  .asDriver(onErrorJustReturn: 0)

これに置き換えましょう。

MetronomeViewModel.swift
numeratorValue = Observable
  .combineLatest(steppedNumerator,
                 maxNumerator.asObservable())
  .map(min)
  .distinctUntilChanged()
  .asDriver(onErrorJustReturn: 0)

単にsteppedNumerator値を取得してそれを返すのではなく、steppedNumeratorの最新値とmaxNumeratorを結合し、2つの値のうちの小さい方にマッピングします。

Command-Uを押してテストスイートをもう一度実行すると、美しく実行された10のテストが表示されます。 素晴らしい仕事ですね!

test16.jpg

Time-Sensitive Testing

あなたはビューモデルをテストすることでかなり遠くになっています。 カバレッジレポートを見ると、ビューモデルの約78%のテストカバレッジがあることがわかります。 それを一番上に向ける時です!

コードカバレッジを表示するには、SchemeポップアップからEdit Scheme ...を選択し、TestセクションでOptionsタブを選択し、Code Coverageをオンにします。 Gather coverage for some targetsを選択肢し、Raytronomeターゲットをリストに追加します。 次のテストが実行されると、カバレッジデータがレポートナビゲータで表示されます。

このチュートリアルを終わらせるためにテストする最後の2つの部分があります。 最初のものは実際に発生したbeatをテストしています。

いくつかのmeter/signatureを指定すると、beatが均等な間隔で放出され、beat自体も正しいことをテストしたいとします(各ラウンドの最初のbeatは残りのものとは異なります)。

最速のdenominatorをテストすることから始めます.32. RaytronomeTests.swiftに戻り、次のテストを追加します。

RaytronomeTests.swift
func testBeatBy32() {
  // 1
  viewModel = MetronomeViewModel(initialMeter: Meter(signature: "4/32"),
                                 autoplay: true,
                                 beatScheduler: scheduler)

  // 2
  let beat = scheduler.createObserver(Beat.self)
  viewModel.beat.asObservable()
    .take(8)
    .bind(to: beat)
    .disposed(by: disposeBag)

  // 3
  scheduler.start()

  XCTAssertEqual(beat.events, [])
}

このテストはまだ合格していません。 しかしそれをさらに小さな断片に分割します。

  1. この特定のテストのために、あなたはいくつかのオプションを使用してビューモデルを初期化します。 あなたは4/32メートルで始まり、あなたのtappedPlayPauseの入力をトリガーする手間を節約する、自動的にビートを発光開始するビューモデルを教えてください。
    3番目の議論も重要です。 デフォルトでは、ビューモデルは`SerialDispatchQueueScheduler`を使用してアプリケーションのビートをスケジュールしますが、実際にビートをテストするときには独自のTestSchedulerを注入して、ビートが適切に放出されるようにします。

  2. beatの型のTestableObserverを作成し、ビューモデルからbeat出力の最初の8Pビートを記録します。8`ビートは2ラウンドを表し、すべてが適切に放出されることを確認するのに十分でなければならない。

  3. schedulerを開始します。 空の配列を宣言していることに注意してください。テストが失敗することをわかっています - 主に値と時間を確認することです。

Command-Uを押してテストを実行します。 アサーションの出力は次のようになります

XCTAssertEqual failed: ("[next(first) @ 1, next(regular) @ 2, next(regular) @ 3, next(regular) @ 4, next(first) @ 5, next(regular) @ 6, next(regular) @ 7, next(regular) @ 8, completed @ 8]") is not equal to ("[]") —

あなたのイベントは正しい値を出しているようですが、時代はちょっと変わったようですね。 単に1から8までの数字のリストです。

これが理にかなっていることを確認するには、メーターを4/32から4/4に変更してみてください。 これはbeat自体が異なるので、異なる時間を生み出すはずです。

Meter(signature: "4/32")Meter(signature: "4/4")に置き換え、Command-Uを押してテストを再実行してください。 まったく同じ時間で、まったく同じアサーションエラーが発生するはずです。

うわー、これは変だ! 放出されたイベントの時刻はまったく同じであることに注意してください。 いわゆる「同じ時間」に2つの異なるsignaturesがどのように放出されるのでしょうか? これは、このチュートリアルの前半で説明したVirtualTimeUnitに関連しています。

Stepping Up the Accuracy

デフォルトのtempo120 BPMを使用し、4のdenominator(4/4など)を使用すると、0.5秒ごとにbeatが得られます。 32のdenominator(4/32など)を使うと、0.0625秒ごとにビートが得られます。

これがなぜ問題なのかを理解するには、TestSchedulerが「real time」を内部的にVirtualTimeUnitに変換する方法を理解する必要があります。

アップ結果のresolution(解像度)roundingと呼ばれるもので、実際の秒を分割することで仮想時間を計算します。 resolutionTestSchedulerの一部であり、デフォルトは1です。

0.0625 / 1の切り上げは1になりますが、0.5 / 1の切り上げも1に等しくなります。これは単にこの種のテストとしては十分正確ではありません。

幸いにも、解像度(resolution)を変更することができ、このような時間に敏感なテストの精度が向上します。

ビューモデルのインスタンス化の上に、テストの最初の行に次の行を追加します。

RaytronomeTests.swift
scheduler = TestScheduler(initialClock: 0, resolution: 0.01)

これにより、仮想時間を切り上げながら、解像度(resolution)が低下し、精度が向上します。

resolutionを落とすときに、仮想時間がどのように異なっているかに注目してください。

test10.png

view modelのイニシャライザでmeterを4/32に戻し、Command-Uを押してテストを再実行します。

あなたは最終的にassertにより洗練されたタイムスタンプを取り戻すでしょう:

XCTAssertEqual failed: ("[next(first) @ 6, next(regular) @ 12, next(regular) @ 18, next(regular) @ 24, next(first) @ 30, next(regular) @ 36, next(regular) @ 42, next(regular) @ 48, completed @ 48]") is not equal to ("[]") —

beatは、仮想時間が6で等間隔です。既存のXCTAssertEqualを次のように置き換えることができます

RaytronomeTests.swift
XCTAssertEqual(beat.events, [
  .next(6, .first),
  .next(12, .regular),
  .next(18, .regular),
  .next(24, .regular),
  .next(30, .first),
  .next(36, .regular),
  .next(42, .regular),
  .next(48, .regular),
  .completed(48)
])

Command-Uを押してもう一度テストを実行すると、最終的にこのテストが表示されます。 優秀ですね!

同じ方法を使って4/4beatをテストするのは非常に似ています。

次のテストを追加します。

RaytronomeTests.swift
func testBeatBy4() {
  scheduler = TestScheduler(initialClock: 0, resolution: 0.1)

viewModel = MetronomeViewModel(initialMeter: Meter(signature: "4/4"), autoplay: true, beatScheduler: scheduler)

let beat = scheduler.createObserver(Beat.self) viewModel.beat.asObservable() .take(8) .bind(to: beat) .disposed(by: disposeBag)

scheduler.start()

XCTAssertEqual(beat.events, [ .next(5, .first), .next(10, .regular), .next(15, .regular), .next(20, .regular), .next(25, .first), .next(30, .regular), .next(35, .regular), .next(40, .regular), .completed(40) ]) }

ここでの唯一の違いは、4 denominatorに十分な精度を提供するので、resolutionを0.1まで上げたことです。

最後にCommand-Uを押してテストスイートを実行してください。この時点で12個のテストがすべて終了するはずです。

view modelのカバレッジを調べると、MetronomeViewModelの99.25%のカバレッジがあることに気付くでしょう。これは優れています。テストされていない出力は、beatTypeのみです。

test11.png

beat typeをテストすることは、この時点では良い挑戦になるでしょう。それは、ビートタイプが.even.oddの間で交互になることを除いて、前の2つのテストと非常に似ているべきであるからです。 あなた自身でそのテストを書くことを試みてください。 あなたが立ち往生した場合は、以下の「Reveal2」ボタンを押して答えを明らかにしてください:

Reveal2

Reveal

RaytronomeTests.swift
func testSignatureStartsAt4By4() throws {
  XCTAssertEqual(try viewModel.signatureText.toBlocking().first(), "4/4")
}

func testTempoStartsAt120() throws { XCTAssertEqual(try viewModel.tempoText.toBlocking().first(), "120 BPM") }

Reveal2

RaytronomeTests.swift
func testBeatTypeAlternates() {
  scheduler = TestScheduler(initialClock: 0, resolution: 0.1)

viewModel = MetronomeViewModel(initialMeter: Meter(signature: "4/4"), autoplay: true, beatScheduler: scheduler)

let beatType = scheduler.createObserver(BeatType.self) viewModel.beatType.asObservable() .take(8) .bind(to: beatType) .disposed(by: disposeBag)

scheduler.start()

XCTAssertEqual(beatType.events, [ .next(5, .even), .next(10, .odd), .next(15, .even), .next(20, .odd), .next(25, .even), .next(30, .odd), .next(35, .even), .next(40, .odd), .completed(40) ]) }

という事で以上でチュートリアルは終わりです。