RxJS Marble Test 入門

When

2016/12/08

At

[Meguro.es #7 @ Livesense](https://meguroes.connpass.com/event/42003/)

---

# 今回の(出典)GitHubリポジトリ

[ovrmrw/rxjs-marble-tests](https://github.com/ovrmrw/rxjs-marble-tests)

===

話す内容

- RxJS 5の**Observable**をどうやってテストするか。

---

## 参考になるサイト

- [Writing Marble Tests (RxJS公式)](https://github.com/ReactiveX/rxjs/blob/master/doc/writing-marble-tests.md)
- [RxJS(5.x)で行うテストファーストな機能開発](http://blog.mmmcorp.co.jp/blog/2016/06/25/testing-rxjs-5/)
- 「rxjs marble test」でググる。ただし英語でも情報は少ない。

---

## 自己紹介

ちきさん
(Tomohiro Noguchi)

Twitter/GitHub/Qiita: [@ovrmrw](https://twitter.com/ovrmrw)

ただのSIer。

Angular Japan User Group (ng-japan)スタッフ。

![3a2512bb-aa72-4515-af42-1f1721252f39.jpg](https://qiita-image-store.s3.amazonaws.com/0/74793/c753b39c-1581-ea1d-87d1-181f37bda60d.jpeg)

---

### アカウントIDの由来

1. the day after tomorrow 
1. overmorrow(俗語)
1. 略して
1. ovrmrw
1. 「先を見据えて」よりさらに先を、みたいな感じです。

(よく聞かれるので:innocent:)

---

### :tada: RxJS 5はとうとう来週stableがリリースされます！ :tada:

---

(ここから本編)

---

あるところにこういうObservableがありました。

```ts
  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`

```ts
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$

```ts
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

```ts
const expected = '---B-C';
const values = { A: 10, B: 20, C: 30 };
```

テスト用語で言うところの **Expected** を書きます。

`expected`の記号に`values`の値を当てはめていくと考えてください。

この例ではこのような結果を期待しています。

- 10ms時には**何も流れてこない**だろう。
- 30ms時には値 `20` が流れてくるだろう。
- 50ms時には値 `30` が流れてくるだろう。

---

## actual$

```ts
const actual$: Observable<number> = mapFilterTest(source$);
```

```ts:(mapFilterTest再掲)
function mapFilterTest(observable: Observable<number>): Observable<number> {
  return observable
    .map(value => value * 10)
    .filter(value => value > 10);
}
```

```ts:(source$再掲)
const source$: Observable<number> = cold<number>('-a-b-c', { a: 1, b: 2, c: 3 });
```

テスト用語で言うところの **Actual** を書きます。

この結果が先程の Expected と一致するかどうかをテストしたいわけですね。

---

## expectObservable, flush

```ts
ts.expectObservable(actual$).toBe(expected, values);
ts.flush();
```

説明は後回しになりますが、`ts`はRxJSの`TestScheduler`クラスのインスタンスです。

Observableである`actual$`が、`expected`と`values`の組み合わせと一致することを期待しています。

`flush`で定義済みのストリームを流します。"Run"みたいな意味合いです。

---

今まで説明してきたことを`it`で書くとこんな感じになります。

```ts
  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`クラスを使うのが肝です。

```ts
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

```ts
  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を全てコンソール出力すると結果はこうなります。一致していないことがわかりますね。

```js
========== 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
    }
  }
]
```

---

ちなみにこのような関数を自作すると、前ページのような出力結果を得られます。

```ts
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を書いてみると、自分が思ってた挙動と違うときがあったりするので救われる:wink:

---

(今日気付いたこと)

- Marble Testの中に非同期(Promise等)が混入すると期待通りのテスト結果は得られません。おそらくMarble Testは非同期に非対応です。(自信は無い)

---

## Thanks!