Marble testing in RxJS 5

背景

  • RxJSには様々なオペレーターがある
  • 文章で説明するのは難しい
  • 絵で表すとわかりやすくなる

Switchオペレーターのドキュメントにはこのような説明がある。

 

convert an Observable that emits Observables into a single Observable that emits the items emitted by the most-recently-emitted of those Observables

絵で表すとわかりやすくなる

マーブルテストって何?

  • マーブルダイアグラムをコードで表す
  • RxJS 5のTestSchedulerで実装されています
  • マーブルダイアグラムはわかりやすいのでテストは読みやすくなる
  • 使ってみましょう!

TestScheduler

import { TestScheduler } from 'rxjs';
  
const scheduler = new TestScheduler(null);
const observable
  = scheduler.createHotObservable('---a---b---c---|');
  
observable.subscribe(x => console.log(x));
  
scheduler.flush();

値を発火する

import { TestScheduler } from 'rxjs';
  
const scheduler = new TestScheduler(null);
const observable
  = scheduler.createHotObservable('---a---b---c---|', { a: 10, b: 20, c: 30 });
  
observable.subscribe(x => console.log(x)); // 10 20 30
  
scheduler.flush();

エラーを出す

import { TestScheduler } from 'rxjs';
  
const scheduler = new TestScheduler(null);
const observable
  = scheduler.createHotObservable('---#', null, 'hoge');
  
observable.subscribe(null, err => console.log(err))
  
scheduler.flush(); // hoge

マーブルで使える文字

- "-" 何も発火せずに10フレームを過ぎる

- "|" completeが発火される

- "#" エラーを出す

- "a-z" nextが発火される

- "(ab)" 同じフレームに発火される

- "^" subscribeされたフレーム(Hot Observableのみ)

テストを書きましょう!

import test from 'ava';
import { TestScheduler } from 'rxjs';

test('merge', (t) => {
  const scheduler = new TestScheduler(t.deepEqual.bind(t));
  const e1 = scheduler.createHotObservable('-a----b|');
  const e2 = scheduler.createHotObservable('-c-----d|');
  const expected =                         '-(ac)-bd|';

  scheduler.expectObservable(e1.merge(e2)).toBe(expected);
  scheduler.flush();
});
test('map', (t) => {
  const scheduler = new TestScheduler(t.deepEqual.bind(t));
  const values = {
    a: { userId: 'a' },
    b: { userId: 'b' },
    c: { userId: 'c' },
    x: 'a',
    y: 'b',
    z: 'c',
  };
  const e1 = scheduler.createHotObservable('-a--b--c|', values);
  const expected =                         '-x--y--z|';

  scheduler.expectObservable(e1.map(v => v.userId)).toBe(expected, values);
  scheduler.flush();
});

test('filter', (t) => {
  const scheduler = new TestScheduler(t.deepEqual.bind(t));
  const e1 = scheduler.createHotObservable('-a--b--aa-b|');
  const expected =                         '----b-----b|';

  scheduler.expectObservable(e1.filter(v => v !== 'a')).toBe(expected);
  scheduler.flush();
});

Mockとして使う

  • Observableをモックできるようなコードを書かないといけません。
  • 特にユーザーの入力やAPIのリスポンスをモックできないとテストしづらいです。
function model(query$, COUNTRIES) {
  return query$
    .map((query) => {
      if (query.length < 2) {
        return { countries: [] };
      }
      const countries = COUNTRIES.filter(
        country => country.toLowerCase().indexOf(query.toLowerCase()) === 0,
      );
      return {
        countries,
      };
    })
    .startWith({ countries: [] });
}

export default model;

シンプルなアプリケーションなので、一つのObservableでステートを管理できる。

componentDidMount() {
  if (this.textInput) {
    const query$ = Observable
      .fromEvent(this.textInput, 'keyup')
      .debounceTime(300)
      .map(e => e.target.value);

    this.state$ = model(
      query$,
      COUNTRIES,
    );

    this.state$.subscribe(this.setState.bind(this));
  }
}
import test from 'ava';
import { TestScheduler } from 'rxjs';
import model from './model';

const COUNTRIES = [
  'Japan',
  'China',
  'Sweden',
  'Switzerland',
  'United States',
  'Germany',
  'Egypt',
];

test('model', (t) => {
  const scheduler = new TestScheduler(t.deepEqual.bind(t));

  const values = {
    a: '',
    b: 'hoge',
    c: 's',
    d: 'sw',
    e: 'ja',
    f: { countries: [] },
    g: { countries: ['Sweden', 'Switzerland'] },
    h: { countries: ['Japan'] },
  };

  const e1 = scheduler.createHotObservable('--a--b-c--d--e--|', values);
  const expected =                         'f-f--f-f--g--h--|';

  scheduler.expectObservable(model(e1, COUNTRIES)).toBe(expected, values);
  scheduler.flush();
});

ご清聴ありがとうございました!

Marble testing in RxJS5

By Andreas A

Marble testing in RxJS5

Some simple marble testing.

  • 904