React/Redux構成のアプリケーションのフロントユニットテストをJestEnzymeで実施する方法をご紹介します。

Jest & Enzymeのインストール

  • Jest:`yarn add –dev jest `または `npm install –save-dev jest`
  • Enzyme: 利用するreactのバージョンに合わせてenzymeとadapterをインストール
    例:reactバージョンが16.xであった場合
    `yarn add –dev enzyme enzyme-adapter-react-16` または `npm i –save-dev enzyme enzyme-adapter-react-16`
  • 注意:babelでトランスパイルできるようにbabel-jestをインストールしてください。
    (`npm install –save-dev babel-jest babel-core` または `yarn add –dev babel-jest babel-core`)

Reducer test
Reducerは純粋関数(同じ引数を渡されたら同じデータを返す)であるため、ここでmock作成は不要です。Jestはデフォルト「`test`」のフォルダーの配下にある.`spec .js` 又は` .test.js`の拡張のファイルを探してくれます。例えば、「`src/store/topics/reducer.js `」のフォルダーにreducerを置きた場合、テストのフォルダーは「`src/__test__/store/topics/reducer.spec.js.`」のように作られます。最もシンプルなテストはreducerの初期値の検証です。こちらは初期の状態(`state`)をinitialStateで定義します。

import uut from '../store/topics/reducer';

const initialState = {
  topicsByUrl: undefined,
  selectedTopicUrls: [],
  selectionFinalized: false,
};

describe('store/topics/reducer', () => {
  it('should have initial state', () => {
  expect(uut()).toEqual(initialState);
  });
});

例えば、一つのアクションを追加するテストコードは以下です。

import Immutable from 'seamless-immutable';
import { Reducer } from 'redux-testkit';
import uut from '../store/topics/reducer';
import * as actionTypes from '../constants/actionTypes';

const initialState = {
  topicsByUrl: undefined,
  selectedTopicUrls: [],
  selectionFinalized: false,
};

describe('store/topics/reducer', () => {

  it('should have initial state', () => {
    expect(uut()).toEqual(initialState);
  });

  it('should not affect state', () => {
    Reducer(uut).expect({type: 'NOT_EXISTING'}).toReturnState(initialState);
  });

  it('should store fetched topics', () => {
    const topicsByUrl = {url1: 'topic1', url2: 'topic2'};
    const action = {type: actionTypes.TOPICS_FETCHED, topicsByUrl};
    Reducer(uut).expect(action).toReturnState({...initialState, topicsByUrl});
  });

  it('should store fetched topics and override existing topics', () => {
    const existingState = Immutable({...initialState, topicsByUrl: {url3: 'topic3'}});
    const topicsByUrl = {url1: 'topic1', url2: 'topic2'};
    const action = {type: actionTypes.TOPICS_FETCHED, topicsByUrl};
    Reducer(uut).withState(existingState).expect(action).toReturnState({...initialState, topicsByUrl});
  });
});

objectが複雑な場合、redux-testkit ではnested state用のメソッドを提供したり、custom expectationsを利用(参考リンク)することもできます。

Actionのテスト
Thunks wrap sync または async function でアクションを実行します。サーバからのデータを取得する過程がActionに含まれるため mock functionは必須です。

import reducer from '../store/topics/reducer';
import * as actions from '../actions/posts/getPost';
import { UPDATE_POST_SUCCESS } from '../actions/posts/updatePost';
import getPostMock from '../mocks/getPostMock';

describe('post reducer', () => {
  it('should return the initial state', () => {
  expect(reducer(undefined, {})).toEqual({});
  });

  it('should handle GET_POST_START', () => {
    const startAction = {
    type: actions.GET_POST_START
  };
  // it's empty on purpose because it's just starting to fetch posts
  expect(reducer({}, startAction)).toEqual({});
  });

  it('should handle GET_POST_SUCCESS', () => {
    const successAction = {
    type: actions.GET_POST_SUCCESS,
    post: getPostMock.data, // important to pass correct payload, that's what the tests are for 😉
  };
  expect(reducer({}, successAction)).toEqual(getPostMock.data);
  });

  it('should handle UPDATE_POST_SUCCESS', () => {
    const updateAction = {
    type: UPDATE_POST_SUCCESS,
    post: getPostMock.data,
  };
  expect(reducer({}, updateAction)).toEqual(getPostMock.data);
  });

  it('should handle GET_POST_FAIL', () => {
    const failAction = {
    type: actions.GET_POST_FAIL,
    error: { success: false },
  };
  expect(reducer({}, failAction)).toEqual({ error: { success: false } });
  });
});

 コンポーネントテスト

1. スナップショットテスト
Jestはスナップショットテスト(snapshot)という機能を持っており、コンポーネントのレンダリング結果を`__snapshot__`フォルダにキャプチャ(capture)します。スナップショットされたコンポーネントに何か変更があった場合は、コンポーネントのテストケースにエラーが発生します。例えば、Linkというコンポーネントを用意します。

// Link.react.js
import React from 'react';

const STATUS = {
  HOVERED: 'hovered',
  NORMAL: 'normal',
};

export default class Link extends React.Component {
  constructor(props) {
  super(props);

  this._onMouseEnter = this._onMouseEnter.bind(this);
  this._onMouseLeave = this._onMouseLeave.bind(this);

  this.state = {
    class: STATUS.NORMAL,
  };
}

  _onMouseEnter() {
    this.setState({class: STATUS.HOVERED});
  }

  _onMouseLeave() {
    this.setState({class: STATUS.NORMAL});
  }

  render() {
    return (
      <a
        className={this.state.class}
        href={this.props.page || '#'}
        onMouseEnter={this._onMouseEnter}
        onMouseLeave={this._onMouseLeave}
      >
      {this.props.children}
      </a>
    );
  }
}

` toMatchSnapshot()` メソッドでスナップショットを作ります。

import React from 'react';
import Link from '../Link.react';
import renderer from 'react-test-renderer';

test('Link changes the class when hovered', () => {
  const component = renderer.create(
  <Link page="http://www.facebook.com">Facebook</Link>,
  );
  let tree = component.toJSON();
  expect(tree).toMatchSnapshot();

  // manually trigger the callback
  tree.props.onMouseEnter();
  // re-rendering
  tree = component.toJSON();
  expect(tree).toMatchSnapshot();

  // manually trigger the callback
  tree.props.onMouseLeave();
  // re-rendering
  tree = component.toJSON();
  expect(tree).toMatchSnapshot();
});

全てのsnapshootを作り直す場合には次のコマンドを使います。
` jest -u `

2. DOM テストについてはenzyme shallow rendererを利用します。
DOM treeの要素(element)をテストに使います。
次のコンポーネントを例にあげます。

// CheckboxWithLabel.js

import React from 'react';

export default class CheckboxWithLabel extends React.Component {
  constructor(props) {
  super(props);
  this.state = {isChecked: false};

// bind manually because React class components don't auto-bind
// http://facebook.github.io/react/blog/2015/01/27/react-v0.13.0-beta-1.html#autobinding
  this.onChange = this.onChange.bind(this);
}

  onChange() {
    this.setState({isChecked: !this.state.isChecked});
  }

  render() {
  return (
    <label>
    <input
      type="checkbox"
      checked={this.state.isChecked}
      onChange={this.onChange}
    />
      {this.state.isChecked ? this.props.labelOn : this.props.labelOff}
    </label>
    );
  }
}

コンポーネントはこのようにテストされます。

// __tests__/CheckboxWithLabel-test.js

import React from 'react';
import {shallow} from 'enzyme';
import CheckboxWithLabel from '../CheckboxWithLabel';

test('CheckboxWithLabel changes the text after click', () => {
// Render a checkbox with label in the document
  const checkbox = shallow(<CheckboxWithLabel labelOn="On" labelOff="Off" />);

  expect(checkbox.text()).toEqual('Off');

  checkbox.find('input').simulate('change');

  expect(checkbox.text()).toEqual('On');
});

この記事を参考に React/Reduxのユニットテストに活用いただけると嬉しいです。
引用元:Viblo