Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
13
Help us understand the problem. What is going on with this article?
@ovrmrw

RxJS Marble Test 入門

More than 3 years have passed since last update.

RxJS Marble Test 入門

by ovrmrw
1 / 24

When

2016/12/08

At

Meguro.es #7 @ Livesense


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

ovrmrw/rxjs-marble-tests

===

話す内容

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

参考になるサイト


自己紹介

ちきさん
(Tomohiro Noguchi)

Twitter/GitHub/Qiita: @ovrmrw

ただのSIer。

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

3a2512bb-aa72-4515-af42-1f1721252f39.jpg


アカウントIDの由来

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

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


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


(ここから本編)


あるところにこういう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$);
(mapFilterTest再掲)
function mapFilterTest(observable: Observable<number>): Observable<number> {
  return observable
    .map(value => value * 10)
    .filter(value => value > 10);
}
(source$再掲)
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$が、expectedvaluesの組み合わせと一致することを期待しています。

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'));
}

テスト失敗時にactualexpectedの配列をJSON.stringifyして出力しています。


Marble Testを書いてみると、自分が思ってた挙動と違うときがあったりするので救われる:wink:


(今日気付いたこと)

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

Thanks!

13
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ovrmrw
ちきさんです。ただのWebエンジニアです。
opt
"INNOVATION AGENCY" を標榜するインターネット広告代理店。エンジニア組織 "Opt Techonologies" を中心にアドテクetc...に取り組んでいます。

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
13
Help us understand the problem. What is going on with this article?