技術探し

技術探し

JavaScriptを中心に記事を書いていきます :;(∩´﹏`∩);:

react-router, redux-sagaのテストの書き方

今回は、自分のelectronのテンプレートを参考にして話したいと思います。

github.com

記事が長くなってしまうので、プロダクションのコードは折りたたみしておきます。

使用ライブラリ

  • Jest
  • Enzyme
  • jest-serializer-enzyme(enzymeでsnapshotとるため)
  • redux-mock-store
  • redux-saga-test-plan

enzymeですが、snapshotに対応してないため react-test-rendererが最近の流れかもしれません。(むしろ推奨?)

https://facebook.github.io/jest/docs/tutorial-react.html#contentfacebook.github.io

github.com


react-router

github.com

react-router@4とreact-router-redux@5を使っています。

Routes.js

各パスをルーティングする部分です。
親がこのファイルをimportします。

import Routes from '../Routes';

const Root = () => (
  <Provider store={store}>
    <ConnectedRouter history={history}>
      <Routes />
    </ConnectedRouter>
  </Provider>
);

このようにファイルを分離することによりテストをしやすくします。

テストコード

import React from 'react';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router';
import configureStore from 'redux-mock-store';
import { render } from 'enzyme';
import Routes from '../../src/renderer/routes';
import rootReducer from '../../src/renderer/reducers';

describe('routes', () => {
  const createDOM = (path = '/') => {
    const state = createStore(rootReducer).getState();
    const store = configureStore()(state);

    return render(
      <Provider store={store}>
        <MemoryRouter initialEntries={[path]}>
          <Routes />
        </MemoryRouter>
      </Provider>
    );
  };

  it('should render the root page', () => {
    expect(createDOM()).toMatchSnapshot();
  });


  it('should render the login page', () => {
    expect(createDOM('/login')).toMatchSnapshot();
  });
});

4系から入ったMemoryRouterを使い、テストをします。
基本的に、スナップショットで比較するだけで自分はいいと思っています。(もちろん確認でwrapper内に対してfind, contains等してもいいとは思います)
react-router-reduxを使っている場合、storeに入れるrouteのlocationのkeyがテスト毎ごとに異なるので、mountではなくrenderを使います。


Redux-saga

github.com

非同期処理をmiddlewareで行うために使います。

Root

各ファイル(e.g. ページ毎)のforkを一括で行います。

テストコード

import rootSaga from '../../../src/renderer/sagas';

describe('root saga', () => {
  it('should register sagas', () => {
    const tasks = 2;

    expect(rootSaga().next().value.length).toEqual(tasks);
  });
});

昔は、rootSaga()._invoke().value.length で取れていまいたが、今は取れません。
ここでは登録されているタスク数(各ファイル数)の個数を確認します。

Partial

それぞれのファイル内のタスクのビジネスロジックが書かれます。
このファイルのrootでそれぞれのactionとタスクを紐付けし、その処理を追えた後再度アクションを発行します。

テストコード

import { expectSaga } from 'redux-saga-test-plan';
import auth from '../../../src/renderer/sagas/auth';

describe('auth saga', () => {
  const storeState = {
    auth: {
      mail: 'a@b.com'
    }
  };

  it('should take on the LOGIN action', () => {
    return expectSaga(auth)
      .withState(storeState)
      .put({
        type   : 'LOGIN_SUCCESS',
        payload: {
          mail: '--a@b.com'
        }
      })
      .dispatch({
        type: 'LOGIN',
        mail: '--a@b.com'
      })
      .run();
  });

  it('should fail on the LOGIN action', () => {
    return expectSaga(auth)
      .withState(storeState)
      .put({
        type : 'ERROR',
        error: {
          code: 'ERROR_LOGIN'
        }
      })
      .dispatch({
        type: 'LOGIN',
        mail: 'a@b.com'
      })
      .run();
  });

  it('should take on the LOGOUT action', () => {
    return expectSaga(auth)
      .withState(storeState)
      .put({ type: 'LOGOUT_SUCCESS' })
      .dispatch({ type: 'LOGOUT' })
      .run();
  });

  it('should fail on the LOGOUT action', () => {
    return expectSaga(auth)
      .withState({
        auth: {
          mail: ''
        }
      })
      .put({
        type : 'ERROR',
        error: {
          code: 'ERROR_LOGOUT'
        }
      })
      .dispatch({ type: 'LOGOUT' })
      .run();
  });
});

redux-saga-test-planを使ってテストしていきます。
基本的にdispatchして、putでyieldされた結果(reducerへ渡す部分)を期待します。
自分はエラー系を全部sagaとして一枚挟んでいます。
template-electron/error.js at master · my-dish/template-electron · GitHub

Selectors

saga内で状態がほしいときに取得する関数を定義します。

テストコード

import { createStore } from 'redux';
import rootReducer from '../../../src/renderer/reducers';
import * as selectors from '../../../src/renderer/sagas/selectors';

describe('selectors', () => {
  const storeState = createStore(rootReducer).getState();

  it('should get auth.email', () => {
    expect(selectors.getMail(storeState)).toEqual('');
  });
});

自分はselectorsに関してはredux-saga-test-planを使いません。
selectorsは単純な関数であるので、stateだけしっかりしていればいいかなと思っているからです。


さいごに

storeでredux-devtools-extensionとかを使っているとテストできない箇所が出てしまうのどうにかならないかな。。。

今月の28日、Node学園 26時限目やりますので是非きてくださいね!(募集は26から)
react, vue, angular, jsconf and Node Collaboratos Summit, webpack3(多分…), etc…の話が聞ける予感する 😁 nodejs.connpass.com