RxJS(5.x)で行うテストファーストな機能開発
小飼です。
『The Next Great Burger』にハマっています。
RxJS
、使っていますか?
弊社では現在開発しているアプリケーションから、本格的にRxJS
を導入して使い込んでいっています。
イベントを配列のように操作できるという高度な抽象化の恩恵で、非常にリーダブルかつ簡潔に機能実装が出来るところが非常に良いですね。
学び始めた当初はReactive Programming
的な考え方で実装を発想するということができず、非常に難解なイメージを持っていましたが、
半年くらい置いて改めて使ってみたところ、非常に使いやすいライブラリであるというイメージにガラッと変わりました。
これはRP
っぽい発想の転換ができたことと併せて、TypeScript
でRxJS
を用いることで、静的型解析の恩恵を受ける事ができた事も大きいと思っています。
(少なくともAPIの多さは問題にならなくなりました)
さて、私はRxJS(というかRP)
を学ぶにあたってこの辺りのコンテンツを読みました。
- Reactive Programming with RxJS
- Introduction to Rx
- 【翻訳】あなたが求めていたリアクティブプログラミング入門
- 関数型プログラマのための Rx 入門(前編)
特に実際のコード例に関してはReactive Programming with RxJS
を主に参考にしていましたが、
これはRxJS 4.x~
に基づいた書籍ですので、現在のベータリリースが公開されている最新版のRxJS 5.x~
とは詳解されているAPIが随分変わっています。
基本的な概念の理解の用には今でも充分に足ると思いますが(ここがRP
入門の最大の関門という気がします)、
特にテストの書き方については公式のドキュメントが用意されておらず、少し手探りで調べる必要がありました。
(RxJSのコントリビュータ向けと思しきテスト手法のドキュメントは存在しています)
そこで本稿では、RxJS 5.x~
においてテストを実装するにあたって必要なステップをまとめました。
RxJS
を使って書くことになるのはアプリケーションの機能の中でも最も抽象的・難解な箇所になることが多いので(個人の感想ですが。。。)
しっかりテストできるとうれしいのではないでしょうか。
マーブル記法によるテスト
まず、一番大きく変わっているところはここだと思います。
4.x~
ではReactiveTest
モジュールからexport
されたonNext
,onError
,onCompleted
などのヘルパー関数に
emit
する値とタイミングを渡す形で擬似的にテスト対象のソースとなるObservable
を表現していました。
これを5.x~
ではマーブル記法
という、ある種のDSLでObservable
を表現できるようになっています。
Writing Marble Tests
Reactive Programming
の入門コンテンツでは、Observable
を表現するためにマーブルダイアグラムで発想してみることを推奨していることが多いです。
マーブルダイアグラムに落として込んでObservable
を考えてみるこの手法は非常に有用で、個人的には実装の前に必ず行う思考法になっているのですが、
これをそのままテスト用のソースObservableとして書けるようにしたのが、マーブル記法
です。
後述のDSL
に沿って書かれたマーブル文字列を、テストヘルパー関数に渡すとそれに基づいたObservable
を生成して返してくれるという仕組みです
(今まで手書きでonNext
していたのを、マーブルダイアグラムで直感的に書けるようになった、という感じです)
例えば、---a---b---c---|
は30ms目にa
,70ms目にb
,110ms目にc
をemit
し、150ms目に完了するObseravable
を表しています。
スケジューラ
こちらは4.x~
の頃と大きな変更はないので簡単におさらいしておきます。
現実の時間経過を必要とする機能のテストは実装が困難で、かつテスト自体の実行時間を引き伸ばしていってしまいます。
これを避けるために、RxJS
の内部にはスケジューラ
という実行タイミングを管理するための機構が用意されていて、
テストの時には、このスケジューラを仮想的な時間経過を適用できるものに切り替えて行います。
テストドキュメント
現在、5.x~
の公式ドキュメントには、テスト方法指南のドキュメントはありません。
ただし、RxJS
の実装そのもののテストは掲載されていますので、これを参考にすればテストが書けます。
と言っても、このテストで使っているテストヘルパー関数のいくつか(グローバルな変数として露出しているもの)は、npmモジュールに公開されていません。
(例えばObseravable.mapのテストで使われているcold
関数など)
この辺のヘルパー関数群は、実はTestScheduler
クラスのインスタンスメソッドを薄くラップしているものに過ぎませんので、
公式が内部で使ってるテストランナーを参考にすれば
RxJS
のユーザ側でもテストに利用することができます。
※ごく小さな実装ですし、各位のテスト環境に合わせてヘルパーを作ってくれという方針なのかも知れません。
作例
ざっとテストに必要な概念をご紹介しましたので、実際にコードに起こしてみようと思います。
こんな感じの機能を開発するとして、ユーザの文字入力から適切なタイミングで検索APIを叩くための中間層を実装したいと想定しています。
- ユーザの文字入力による、インクリメンタルサーチ機能実装したい
- 検索リクエストの頻度を適切に抑えるために100msほどのバッファタイムを設ける
- スペースで句切られた文字列は別の単語として検索する(例: queries[]=foo&queries[]=buzz)
それではこの仕様を満たしそうなテストを書いてみます。
まずソースObservableとなる、ユーザ入力をマーブル記法で書いてみます。
// 30msごとに`RxJS5`と入力するObservable
"---a---b---c---d---e-", { a: "R", b: "x", c: "J", d: "S", e: "5" })
次に、これがどういうObseravableにしたいのかをマーブル記法で書きます。
// 100msごとに溜めたすべての文字列を発行するObseravable
"----------f---------g", { f: ["Rx"], g: ["RxJS5"] })
検索ワードがスペースで区切られていた場合のテストケースを作るために、もう一つマーブルを作っておきます
"---a---b---c---d---e-", { a: "r", b: "x", c: " ", d: "j", e: "s" }
"----------f---------g";, { f: ["rx"], g: ["rx", "js"] }
テストコードの全体像はこんな感じになりました。
import { Observable, TestScheduler } from "rxjs";
import * as assert from "assert";
import { createIncrementalSearch } from "../app";
describe("非同期処理のテスト", () => {
let testScheduler, hot, cold;
beforeEach(() => {
// テスト対象のObseravableと、期待するObseravableを比較する関数
// `(actual, expected) => boolean`を渡してTestSchedulerをインスタンス化する
testScheduler = new TestScheduler(assert.deepEqual);
// RxJS本家に倣って、Obseravableのファクトリメッソッドのエイリアスを作る(必須ではない)
cold = testScheduler.createColdObservable.bind(testScheduler);
});
it("100ms毎に検索を実行する", () => {
const input$ = cold("---a---b---c---d---e-", { a: "R", b: "x", c: "J", d: "S", e: "5" });
const expect$ = "----------f---------g";
const test$ = createIncrementalSearch(input$, testScheduler);
testScheduler.expectObservable(test$).toBe(expect$, { f: ["Rx"], g: ["RxJS5"] });
testScheduler.flush();
});
it("スペースで単語を区切られる", () => {
const input$ = cold("---a---b---c---d---e-", { a: "r", b: "x", c: " ", d: "j", e: "s" });
const expect$ = "----------f---------g";
const test$ = createIncrementalSearch(input$, testScheduler);
testScheduler.expectObservable(test$).toBe(expect$, { f: ["rx"], g: ["rx", "js"] });
testScheduler.flush();
});
});
テストが書けたので、これを満たす実装コードを書いてみます。
buffer
に入力値を貯めてから、入力値を畳み込んで文字列に戻しています。
最後にスペースでsplitして完成です。
import { Observable } from "rxjs";
export const createIncrementalSearch = (input$: Observable<string>, testScheduler = null): Observable<string[]> => {
return input$
.bufferTime(100, null, testScheduler)
.filter(c => c.length > 0)
.map(c => c.join(""))
.scan((acc, s) => (acc + s), "")
.map(s => s.split(" "));
};
コードはgithubのレポジトリに上がっています
npm test
で実際にテストが通ることが確認できると思いますので、良かったらこちらも見てみて下さい。
まとめ
RxJS 5.x~
でのテストについて、簡単にまとめてみましたが、いかがでしたでしょうか。
参考になれば幸いです。