概要
RxSwiftでのテストの書き方が分からなかったので海外の記事を参考にすることにしました。
これまで色々なiOSのアプリの開発・運用に携わってきましたが運が悪かったのかテストコードが1行も書かれていないプロジェクトばかりを担当してきました。
もちろん、それはiOSエンジニアのスキルが低いから、というわけではなくそもそも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つの画面しか持たない非常にシンプルなアプリだとわかるでしょう。
アプリをビルドして実行します。 再生ボタンをタップしてメトロノームを開始します。 拍子やテンポを変更することもできます。
このアプリケーションは、単一のView Controller(MetronomeViewController.swift)とMetronomeViewModel.swiftで構成され、すべてのビジネスロジックが含まれています。
The Challenges of Testing Streams
RxSwiftとObservableストリームの基本を簡単に要約します。
ストリームの操作は、基本的な値やオブジェクトを扱う場合と本質的に異なります。 したがって、それらをテストするタスクも異なります。
値は単体で独立しています。それらは時間の表現や概念を持っていません。 一方で観測可能なストリームは、時間の経過と共に要素(例えば、値)を放出します。

つまり値のストリームをテストするときは、次のいずれかをテストする必要があります。
- 一部のストリームは、時間に関係なく特定の要素を出力します。
- 一部のストリームは、特定の時刻に特定の要素を出力します。 この場合、放出された要素をストリームが放出したときと一緒に「記録する」方法が必要になります。
Determining What to Test
あなたが実際にテストしたいことについて考えてみるのは、しばらく時間をとることをお勧めします。
前述のように、メトロノームに関連する実際のビジネスロジックを含むビューモデルであるMetronomeViewModel
をテストします。
MetronomeViewModel.swiftを開きます。 ビューモデルを見ると、分子、分母、シグネチャ、テンポストリング、分子の実際の値、分子の最大値、ビートの原因となるストリームなど、いくつかのロジックを担当する出力を見ることができます。
このアプリはDriverのインスタンスを使用してすべての出力を表します。 ドライバは、UIコンポーネントを扱うときにあなたの人生を楽にする種類のストリームです。

あなたがUIでテストしたいものについて考えてみましょう。クイックリストを作成します。
あなたはそれをテストしたい。
- numeratorとdenominatorは4と4から始まります。
- signatureは4/4から始まります。
- tempoは120から始まります。
- Play/Pauseボタンをタップすると、metronomeの
isPlaying
の状態が変わります。 - numerator、denominatorまたはtempoを変更することで、適切なテキスト表現が生成されます。
- beatはsignatureに合わせて「beating」します。
- beatは
.even
と.oddの間
で交互に表示されます。アプリはこれを使用して、ビューの上部にあるmetronomeのイメージを設定します。
テストを書くときは、RxSwiftにバンドルされているRxBlockingとRxTestという2つの追加のフレームワークを使用します。 それぞれは、ストリームをテストするためのさまざまな機能と概念を提供します。 これらのフレームワークは、すでにあなたのスタータープロジェクトの一部です。
Using RxBlocking
スタータープロジェクトには、RaytronomeTests.swiftファイルを含むbare-bonesのテストターゲットが含まれています。
それを開いて見てください。 RxSwift、RxCocoa、RxTest、RxBlockingをインポートし、viewModel
プロパティと基本的なsetUp()
メソッドを組み込んで、すべてのテストケースの前にビューモデルの新しいインスタンスMetronomeViewModel
を作成します。
最初のテストケースは、numeratorとdenominator の両方が4
の値で始まることを確認することになります。つまり、これらの各ストリームの最初に放出された値のみに注意します。 RxBlockingの完璧な仕事のように思えます!
RxBlockingは、RxSwiftで利用可能な2つのテストフレームワークの1つで、ObservableストリームをBlocking Observableに変換することができます。Blocking Observableは、現在のスレッドをブロックし、オペレータによって指定された特定の条件を待つ特別な観測です。
終了シーケンス(つまり、completed
イベントまたはerror
イベントを発行(emit)するイベント)を処理している状況、または有限数のイベントをテストすることを目的としている場合に役立ちます。
RxBlockingはいくつかの演算子を提供しますが、最も有用なものは...
-
toArray()
:シーケンスが終了するのを待ち、すべての結果を配列として返します。 -
first()
:最初の要素を待ち、それを返します。 -
last()
:シーケンスが終了するのを待ち、放出された最後のアイテムを返します。
これらの演算子を見ると、first()
がこの特定のケースに最も適しています。
次の2つのテストケースをRaytronomeTests
クラスに追加します。
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を押してテストを実行します。

簡単な挑戦として、次の2つのテストを試してみて、signatureText
が4/4
として開始し、tempoText
が120 BPM
として開始することを確認してください。テストは上記の2つのテストとほぼ同じである必要があります。
作業が終わったら、テストスイート全体をもう一度実行し、4回の合格テストに合格することを確認してください。
あなたが立ち往生する場合は、Reveal
ボタンをタップして解決策を覗いてみてください。
Advantages and Disadvantages of RxBlocking
ご存じのように、RxBlockingは非常に優れており、非常によく知られているコンストラクトの下で反応的な概念を「包み込む」ようになっています。 残念ながら、それには注意すべきいくつかの制限があります。
- これは、有限シーケンスをテストすることを目的としています。つまり、完成したシーケンスの最初の要素または要素のリストをテストする場合、RxBlockingは非常に便利です。 しかし、非終端シーケンスを処理するより一般的なケースでは、RxBlockingを使用しても必要な柔軟性は得られません。
-
RxBlockingは、現在のスレッドをブロックし、実際に実行ループをロックすることによって機能します。
Observable
が比較的長い間隔または遅延でイベントをスケジュールする場合、BlockingObservable
は同期問題のイベントを待機します。 - タイムベースのイベントをアサートし、正しいタイムスタンプが含まれていることを確認するのに興味があるときは、RxBlockingは時間だけでなく要素をキャプチャするだけなので、何の助けにもなりません。
- 非同期入力に依存する出力をテストする場合、RxBlockingは、現在のスレッドをブロックするときに有用ではありません。例えば、他の観測可能なトリガを必要とする出力をテストする場合です。
実装する必要がある次のテストは、これらの制限のほとんどに対応しています。 例:Play/Pauseボタンをタップすると、isPlaying
出力が新たに発生します。これには、非同期トリガー(tappedPlayPause
input)が必要です。 また、排出量をテストすることも有益であろう。
Using RxTest
最後のセクションで述べたように、RxBlockingは大きな利点を提供しますが、ストリームのイベント、時間、他の非同期トリガーとの関係を徹底的にテストすることには少し欠けているかもしれません。
これらの問題のすべてを解決するために、RxTestが救助に来ます!
RxTestはRxBlockingとはまったく異なるビーストですが、主な違いは能力とストリームに関する情報の方がはるかに柔軟であることです。 これは、TestScheduler
という独自の特別なスケジューラーを提供するため、これを行うことができます。
コードに入る前に、schedulerが実際に何をしているのか検討する価値があります。
Understanding Schedulers
schedulerは、RxSwiftの下位概念のビットですが、テストでの役割をよりよく理解するためには、schedulerが何であり、どのように機能しているかを理解することが重要です。
RxSwiftはschedulerを使用して、作業をどのように実行するかを抽象的に記述し、その作業の結果として発生するイベントをスケジュールします。
なぜこれが面白いですか、とあなたは思うかもしれません。
RxTestは、TestScheduler
というカスタムスケジューラをテスト専用に提供しています。Observable
とObservers
を作成して、これらのイベントを「record」してテストできるようにすることで、時間ベースのイベントを簡単にテストできます。
schedulersの詳細を知りたい場合は、公式のドキュメントにいくつかの洞察とガイドラインがあります。
Writing Your Time-Based Tests
テストを書く前に、TestScheduler
のインスタンスを作成する必要があります。 また、DisposeBag
をクラスに追加して、テストで作成されるDisposables
を管理します。 viewModel
プロパティの下に、次のプロパティを追加します。
var scheduler: TestScheduler!
var disposeBag: DisposeBag!
次に、setUp()
の最後に次の行を追加して、すべてのテストの前に新しいTestScheduler
およびDisposeBag
を作成します。
scheduler = TestScheduler(initialClock: 0)
disposeBag = DisposeBag()
TestScheduler
の初期化は、ストリームの「開始時刻」を定義するinitialClock
引数を取ります。 新しいDisposeBag
は、以前のテストで残ったサブスクリプションを取り除いてくれます。
実際のtest writingに向かいましょう!
最初のテストでは、Play/Pauseボタンが数回トリガーされ、isPlaying
出力が変更に応じて出力されることをアサートします。
これを行うには、次のことが必要です。
-
tappedPlayPause
入力に偽の「タップ」を発するmockのObservable
なストリームを作成します。 -
Observer
ライクな型を作成して、isPlaying
の出力から放出されたイベントを記録します。 - 記録されたイベントはあなたが期待するイベントであると主張する。
これはたくさんのように見えるかもしれませんが、いかにして一緒になるのかを見ると驚くでしょう!
いくつかの事例を例に挙げて説明します。最初のRxTestベースのテストを追加します。
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)
])
}
これは少し威圧している場合は心配しないでください。 それを壊します。
-
TestScheduler
を使用して、mockにしたい要素の型のTestableObserver
を作成します。この場合、Bool
です。 この特別なobserverの主な利点の1つは、追加されたイベントをアサートするために使用できるevents
プロパティを公開することです。 -
isPlaying
の出力を新しいTestableObserver
にdrive()
します。 ここでイベントを「record」します。 -
tappedPlayPause
入力への3つの「taps」の放出を模倣するモックObservable
を作成します。 ここでも、これは、TestableObservable
という特殊な型のObservable
で、TestScheduler
を使用して、指定された仮想時間にイベントを発生させます。 - テストスケジューラで
start()
を呼び出します。 このメソッドは、以前のポイントで作成されたpendingのサブスクリプションをトリガーします。 -
RxTestにバンドルされた
XCTAssertEqual
の特別なoverloadを使用すると、isPlaying
のイベントを両方の要素と時間で予想されるものと同等にすることができます。10
,20
および30
は、入力が発生した時刻に対応し、0
はisPlaying
の最初の出力です。
混乱するって?このように考えてみましょう。イベントのストリームを「模擬」し、特定の時刻にビューモデルの入力にフィードします。 次に、適切なタイミングで予想されるイベントが出力されるように出力をアサートします。

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

Understanding Time Values
おそらく、0
、10
、20
、30
の値が時間に使用されていることに気がつき、これらの値が実際に意味するものが何なのか不思議だと思うでしょう。 それらが実際の時間とどのように関係していますか?
RxTestは、定期的な時刻(Date
など)をVirtualTimeUnit
(Int
で表される)に変換する内部メカニズムを使用します。
RxTestでイベントをスケジューリングする場合、使用する時間はあなたが望むもので何でもかまいません。それらは完全に任意であり、TestScheduler
は他のスケジューラのようにイベントをスケジューリングするためにそれらを使用します。
この仮想時間は実際の秒と実際には対応していません。つまり、10
は実際には10秒を意味するのではなく、仮想時間のみを表します。 このチュートリアルの後半で、このメカニズムの内部についてもう少し詳しく学びます。
TestScheduler
の時間について深く理解したので、あなたのビューモデルにさらにテストカバレッジを追加するのはどうでしょうか?
前のテストの直後に次の3つのテストを追加します。
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を正しく更新すると、BPM
suffix付きの文字列表現が適切に出力されると主張します。
Command-Uを押してテストを実行し、合計8回の合格テストを残します。 素敵ですね!

OK - あなたはそれのコツを得たように思えます!
それを一歩上げる時です。 次のテストを追加します。
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")
])
}
深呼吸しましょう! これは本当に新しいものか恐ろしいものではありませんが、あなたがこれまでに書いた同じテストのものより長いバリエーションです。 stepppedNumerator
とsteppedDenominator
の両方の入力に要素を連続して追加してすべての種類の異なる拍子記号を作成すると、signatureText
出力が適切に書式設定されたsignatureを出力することをアサーションします。
テストをより視覚的に見ると、これはより明確になります。

あなたのテストスイートを自由に実行してください。 今は9回の合格テストがあります!
次に、より複雑なユースケースに亀裂が生じます。
次のシナリオを考えてみましょう。
- アプリは4/4のsignatureで始まります。
- あなたは24/32のsignatureに切り替えます。
- 次に、denominatorの - ボタンを押します。 24/16、24/8、および24/4がメトロノームに有効なmeterではないため、signatureが16/16に、次に8/8に、そして最終的に4/4に落ちるはずです。
これらのmeterのいくつかは音楽的に有効ですが、あなたはメトロノームのためにそれらを違法とみなします。
このシナリオのテストを追加します。
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
])
}
ちょっと複雑ですが、何も処理できません! それを分割します。
- いつものように、まず
TestableObserver
を作成し、numeratorValue
の出力をそれに送ります。 - ここでは、物事は少し混乱しますが、下の視覚的表現を見ると、それがより明確になります。 32のdenominatorに切り替えることから始まり、次に24分の1のnumeratorに切り替えて、24/32メートルにします。 次に、denominatorを段階的にドロップして、モデルが
numeratorValue
出力の変更を放出するようにします。 -
scheduler
を開始します。 - 適切な
numeratorValue
が各ステップごとに発行されると主張します。

あなたが作ったかなり複雑なテストです。 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です!
あなたがバグを見つけたようです。
もちろん、それを修正する責任を負う選択をします。
MetronomeViewModel.swiftを開き、numeratorValue
の設定を担当する次のコードを見つけます。
numeratorValue = steppedNumerator
.distinctUntilChanged()
.asDriver(onErrorJustReturn: 0)
これに置き換えましょう。
numeratorValue = Observable
.combineLatest(steppedNumerator,
maxNumerator.asObservable())
.map(min)
.distinctUntilChanged()
.asDriver(onErrorJustReturn: 0)
単にsteppedNumerator
値を取得してそれを返すのではなく、steppedNumerator
の最新値とmaxNumerator
を結合し、2つの値のうちの小さい方にマッピングします。
Command-Uを押してテストスイートをもう一度実行すると、美しく実行された10のテストが表示されます。 素晴らしい仕事ですね!
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に戻り、次のテストを追加します。
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, [])
}
このテストはまだ合格していません。 しかしそれをさらに小さな断片に分割します。
-
この特定のテストのために、あなたはいくつかのオプションを使用してビューモデルを初期化します。 あなたは
4/32
メートルで始まり、あなたのtappedPlayPause
の入力をトリガーする手間を節約する、自動的にビートを発光開始するビューモデルを教えてください。
3番目の議論も重要です。 デフォルトでは、ビューモデルは`SerialDispatchQueueScheduler`を使用してアプリケーションのビートをスケジュールしますが、実際にビートをテストするときには独自のTestScheduler
を注入して、ビートが適切に放出されるようにします。 -
beatの型の
TestableObserver
を作成し、ビューモデルからbeat
出力の最初の8Pビートを記録します。
8`ビートは2ラウンドを表し、すべてが適切に放出されることを確認するのに十分でなければならない。 -
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
と呼ばれるもので、実際の秒を分割することで仮想時間を計算します。 resolution
はTestScheduler
の一部であり、デフォルトは1
です。
0.0625 / 1
の切り上げは1
になりますが、0.5 / 1
の切り上げも1
に等しくなります。これは単にこの種のテストとしては十分正確ではありません。
幸いにも、解像度(resolution)
を変更することができ、このような時間に敏感なテストの精度が向上します。
ビューモデルのインスタンス化の上に、テストの最初の行に次の行を追加します。
scheduler = TestScheduler(initialClock: 0, resolution: 0.01)
これにより、仮想時間を切り上げながら、解像度(resolution)が低下し、精度が向上します。
resolutionを落とすときに、仮想時間がどのように異なっているかに注目してください。

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
を次のように置き換えることができます
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/4
beatをテストするのは非常に似ています。
次のテストを追加します。
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
のみです。

beat typeをテストすることは、この時点では良い挑戦になるでしょう。それは、ビートタイプが.even
と.odd
の間で交互になることを除いて、前の2つのテストと非常に似ているべきであるからです。 あなた自身でそのテストを書くことを試みてください。 あなたが立ち往生した場合は、以下の「Reveal2」ボタンを押して答えを明らかにしてください:
Reveal
func testSignatureStartsAt4By4() throws {
XCTAssertEqual(try viewModel.signatureText.toBlocking().first(), "4/4")
}
func testTempoStartsAt120() throws {
XCTAssertEqual(try viewModel.tempoText.toBlocking().first(), "120 BPM")
}
Reveal2
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)
])
}
という事で以上でチュートリアルは終わりです。