When
2016/12/08
At
今回の(出典)GitHubリポジトリ
===
話す内容
- RxJS 5のObservableをどうやってテストするか。
参考になるサイト
- Writing Marble Tests (RxJS公式)
- RxJS(5.x)で行うテストファーストな機能開発
- 「rxjs marble test」でググる。ただし英語でも情報は少ない。
自己紹介
ちきさん
(Tomohiro Noguchi)
Twitter/GitHub/Qiita: @ovrmrw
ただのSIer。
Angular Japan User Group (ng-japan)スタッフ。
アカウントIDの由来
- the day after tomorrow
- overmorrow(俗語)
- 略して
- ovrmrw
- 「先を見据えて」よりさらに先を、みたいな感じです。
(よく聞かれるので)
RxJS 5はとうとう来週stableがリリースされます!
(ここから本編)
あるところにこういうObservableがありました。
Observable.of(1, 2, 3)
.map(value => value * 10)
.filter(value => value > 10)
.do(value => console.log(value));
出力はこうなります。
20
30
map
によって値が10倍されて
filter
によって10より大きい値だけが抽出されました。
これが本当にそうなるかをテストします。
先程のObservableをこのように関数でラップします。
Interface: (observable) => observable
function mapFilterTest(observable: Observable<number>): Observable<number> {
return observable
.map(value => value * 10)
.filter(value => value > 10);
}
テストしたいObservableをこのように書き直すのがRxJSのテストの第一歩です。
Observableをテストするために必要なもの
- source$
- expected, values
- actual$
- expectObservable, flush
source$
const source$: Observable<number> = cold<number>('-a-b-c', { a: 1, b: 2, c: 3 });
cold
はObservableを生成する関数です。他にSubjectを生成するhot
があります。
'-a-b-c'
がMarbleです。一文字目が0msで、その後一文字分が10msの時間の流れを持ちます。
上記の例で言えばこのようなストリームを表します。
- 10ms時に値
1
が流れてきた。 - 30ms時に値
2
が流れてきた。 - 50ms時に値
3
が流れてきた。
expected, values
const expected = '---B-C';
const values = { A: 10, B: 20, C: 30 };
テスト用語で言うところの Expected を書きます。
expected
の記号にvalues
の値を当てはめていくと考えてください。
この例ではこのような結果を期待しています。
- 10ms時には何も流れてこないだろう。
- 30ms時には値
20
が流れてくるだろう。 - 50ms時には値
30
が流れてくるだろう。
actual$
const actual$: Observable<number> = mapFilterTest(source$);
function mapFilterTest(observable: Observable<number>): Observable<number> {
return observable
.map(value => value * 10)
.filter(value => value > 10);
}
const source$: Observable<number> = cold<number>('-a-b-c', { a: 1, b: 2, c: 3 });
テスト用語で言うところの Actual を書きます。
この結果が先程の Expected と一致するかどうかをテストしたいわけですね。
expectObservable, flush
ts.expectObservable(actual$).toBe(expected, values);
ts.flush();
説明は後回しになりますが、ts
はRxJSのTestScheduler
クラスのインスタンスです。
Observableであるactual$
が、expected
とvalues
の組み合わせと一致することを期待しています。
flush
で定義済みのストリームを流します。"Run"みたいな意味合いです。
今まで説明してきたことをit
で書くとこんな感じになります。
it('should return correct observable', () => {
const source$ = cold<number>('-a-b-c', { a: 1, b: 2, c: 3 });
const expected = '---B-C';
const values = { A: 10, B: 20, C: 30 };
const actual$ = mapFilterTest(source$);
ts.expectObservable(actual$).toBe(expected, values);
ts.flush();
});
テストの全容はこのようになります。TestScheduler
クラスを使うのが肝です。
import { Observable, Subject, TestScheduler } from 'rxjs/Rx';
import { assertDeepEqual } from '../testing/helper';
describe('TEST: RxJS Marble Test', () => {
let ts: TestScheduler;
let hot: typeof TestScheduler.prototype.createHotObservable;
let cold: typeof TestScheduler.prototype.createColdObservable;
beforeEach(() => {
ts = new TestScheduler(assertDeepEqual);
hot = ts.createHotObservable.bind(ts);
cold = ts.createColdObservable.bind(ts);
});
it('should return correct observable', () => {
const source$ = cold<number>('-a-b-c', { a: 1, b: 2, c: 3 });
const expected = '-A-B-C';
const values = { A: 10, B: 20, C: 30 };
const actual$ = mapFilterTest(source$);
ts.expectObservable(actual$).toBe(expected, values);
ts.flush();
});
}
Test to fail
it('should return correct observable', () => {
const source$ = cold<number>('-a-b-c', { a: 1, b: 2, c: 3 });
const expected = '-A-B-C'; // 元は'---B-C'
const values = { A: 10, B: 20, C: 30 };
const actual$ = mapFilterTest(source$);
ts.expectObservable(actual$).toBe(expected, values);
ts.flush();
});
わざとテストを落とすためにexpected
を変更してみました。これにより
- 10ms時には値
10
が流れてくるだろう。 - 30ms時には値
20
が流れてくるだろう。 - 50ms時には値
30
が流れてくるだろう。
という結果を期待することになりますが、実際には10ms時には何も流れてきません。
ActualとExpectedを全てコンソール出力すると結果はこうなります。一致していないことがわかりますね。
========== ACTUAL ==========
[
{
"frame": 30,
"notification": {
"kind": "N",
"value": 20,
"hasValue": true
}
},
{
"frame": 50,
"notification": {
"kind": "N",
"value": 30,
"hasValue": true
}
}
]
========== EXPECTED ==========
[
{
"frame": 10,
"notification": {
"kind": "N",
"value": 10,
"hasValue": true
}
},
{
"frame": 30,
"notification": {
"kind": "N",
"value": 20,
"hasValue": true
}
},
{
"frame": 50,
"notification": {
"kind": "N",
"value": 30,
"hasValue": true
}
}
]
ちなみにこのような関数を自作すると、前ページのような出力結果を得られます。
import * as lodash from 'lodash';
import * as assert from 'assert';
export function assertDeepEqual(actual: any, exptected: any): void {
let messages: string[] = ['\n'];
if (!lodash.isEqual(actual, exptected)) {
messages.push('='.repeat(10) + ' ACTUAL ' + '='.repeat(10));
messages.push(JSON.stringify(actual, null, 2));
messages.push('='.repeat(10) + ' EXPECTED ' + '='.repeat(10));
messages.push(JSON.stringify(exptected, null, 2));
messages.push('\n');
}
assert.deepEqual(actual, exptected, messages.join('\n'));
}
テスト失敗時にactual
とexpected
の配列をJSON.stringify
して出力しています。
Marble Testを書いてみると、自分が思ってた挙動と違うときがあったりするので救われる
(今日気付いたこと)
- Marble Testの中に非同期(Promise等)が混入すると期待通りのテスト結果は得られません。おそらくMarble Testは非同期に非対応です。(自信は無い)