Rails + React のプロジェクトに Jest + Enzyme を導入してReactコンポーネントを単体テストする

記事タイトルとURLをコピーする

Reactを利用しているRails プロジェクトにJestとEnzymeを導入して
Reactコンポーネントのテストを書くことができたので導入手順をメモします。

当プロジェクトの環境は導入時点でRails5.2系で、webpackerとsprocketsが共存している状態でした。

Jestとは

jest は javascriptのテストフレームワークです。
describe, it, expect などがあり RSpec と近い構文で記述することができます。

describe("足し算", () => {
  it("1 + 1 = 2 であること", () => {
    expect(1 + 1).toEqual(2);
  });
});

Enzymeとは

Enzymeは jestなどのテストフレームワークを使って、Reactをテストすることができるライブラリです。
レンダリングやクリックなどのイベントをシュミレートすることができます。

import CheckboxWithLabel from '../CheckboxWithLabel';

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

jest/CheckboxWithLabel-test.js at master · facebook/jest · GitHub

Jest の導入

まずはライブラリをインストールします。

$ yarn add --dev jest

次にjestの設定ファイルを作成します。

module.exports = {
  roots: ["."],
  verbose: true,
};

package.jsonに書くこともできますが、分厚くなり過ぎてしまうことを恐れて、今回は別ファイルに分離しました。

ファイルの置き場はプロジェクトのルートに置くことも考えましたが、
プロジェクトルートのファイルが増えると一覧性が悪くなることと、
jsのテストにのみ利用するファイルだったため、既存のjsのテスト(coffee script用の teaspoonなど)が入っていたspec/javascripts/以下に置くことにしました。

package.jsonのscriptに以下を追記します。

"script": {
  "test": "jest --config spec/javascripts/jest.config.js"

--config で、先ほど作成した設定ファイルを読み込ませるようにします。
これで、yarn test でテストを実行できる状態になりました。

Enzyme の導入

$ yarn add --dev enzyme enzyme-adapter-react-16

Enzyme は 利用するreactのバージョンと対応したアダプターを設定する必要があるため、一緒にインストールしています。
アダプターはテストの際に読み込んで設定しておく必要があるため、新しくsetUpTest.jsを作成し、テスト全体の事前処理を設定します。

import { configure } from "enzyme";
import EnzymeAdapter from "enzyme-adapter-react-16";

configure({ adapter: new EnzymeAdapter() });

jestの設定ファイルに上記のファイルを読み込むように追記します。

module.exports = {
  roots: ["."],
  setupFilesAfterEnv: ["<rootDir>/setupTests.js"], // 追記
  verbose: true,
};

これで、テスト実行前にenzymeのアダプター設定が行われるように設定できました。

eslint-plugin-jest の導入

ここまでで実際にreactのテストがかける状態になりましたが、 利用しているeslintがjestのグローバルオブジェクトに警告してしまうという問題がありました。

jestのグローバルオブジェクトがeslintのno-undef警告を受けている
jestのグローバルオブジェクトがeslintのno-undef警告を受けている

importされていないdescribe, it, expectなどが使われているため、eslint側では警告をあげてしまうようです。

これを回避するために追加で、eslint-plugin-jest を導入し設定しました。

$ yarn add --dev eslint-plugin-jest

eslintの設定ファイル .eslintrc.js にも追記します。

"env": {
  "jest/globals": true,
  // ...
},
"plugins": [
  "jest",
  // ...
]

jestの設定にmoduleDirectoriesを追加

また、テストを書く上でテスト対象コンポーネントを読み込む際お互いのファイルの位置関係で 相対パスが多くなり視認性が悪かったため、jest.config.jsmoduleDirectories"../../" (= プロジェクトルート)を設定するようにしました。*1

module.exports = {
  moduleDirectories: ["../../", "node_modules"], // 追加
  roots: ["."],
  setupFilesAfterEnv: ["<rootDir>/setupTests.js"],
  verbose: true,
};

これで次のように書くことができるようになりました。

// before
import GoodComponent from "../../../app/javascript/GoodComponent";
// after
import GoodComponent from "app/javascript/GoodComponent";

enzyme の shallow と mount

import { shallow, mount } from 'enzyme';

const wrapper = shallow(<CheckboxWithLabel labelOn="On" labelOff="Off" />);
const wrapper = mount(<CheckboxWithLabel labelOn="On" labelOff="Off" />);

enzymeではこのようにshallow、またはmount関数を使うことでコンポーネントをラップし、
テスト用のプロパティを参照できるようになります。 shallowとmountの違いとしては

  • shallow

    • 単体テスト向け
    • 子コンポーネントはテストできない
  • mount

    • 子コンポーネントをテストできる
    • DOM APIを利用するコンポーネントをテストできる

という使い分けのようです。

enzymejs.github.io

shallowの方が動作も軽いので基本的にはshallowを利用する方針で良さそうです。

コールバック関数をモックする

コンポーネントにpropsで渡された関数等の呼び出しをテストする際はjestのモック関数を使うと便利です。
jest.fn()で使うことができます。

const mockCallback = jest.fn();
// 関数が呼び出されるような処理を実行させる

it("コールバック関数が一度呼ばれる", () => {
  expect(mockCallbackFunc.mock.calls.length).toBe(1);
});

it("コールバック関数の引数に渡される値はXXXである", () => {
  expect(mockCallbackFunc.mock.calls[0][0]).toBe(XXX);
});

モック関数の内部呼び出しカウンタは累計なので、
リセットしたい場合はafterEachなどでリセット処理を書きます。*2

afterEach(() => {
  // mockCallbackFunc 内部のカウンタをリセットする
  jest.clearAllMocks();
});

jestjs.io

event.PreventDefaultを呼び出す場合

Enzymeではsimulateでイベントをシミュレートした際に、第2引数としてイベントオブジェクトを渡すことができます。
例えばセレクトボックスでオプションを選ぶ時に、選択されたオプションのオブジェクトを渡すことができます。

// セレクトボックスで 「日本語」 を選択する
 wrapper
   .find("select")
   .simulate("change", { target: { value: "日本語" });

逆に、コンポーネント内で、event.preventDefault() を呼び出しているような場合はちゃんとpreventDefaultも定義しておかないとTypeErrorが発生してしまいました。

// セレクトボックスで 「日本語」 を選択する
wrapper
  .find("select")
  .simulate("change", { target: { value: "日本語" }, preventDefault: () => {} });

preventDefaultが呼ばれることもテストする場合はこちらもjest.fn()でモック化しておくと良さそうです。

今後の課題

今回のjest/ enzyme導入で、reactコンポーネントのテスト書くことが可能になりましたが、 このままではテストできないコンポーネントもいくつか発見しました。

例えば、sprocketsを利用してグローバル空間に読み込んでいるjQueryやI18nなどのオブジェクトを reactコンポーネントから利用している場合は、
テスト環境と実際の環境でグローバルに存在するオブジェクトが異なるため、
このままではうまくテストできません。*3

// テスト環境のwindowにはjQueryやI18nがない

const $ = window.jQuery;
const I18n = window.I18n;

これらのコンポーネントへの対応策としては、これまでgemを使ってsprocketsで読み込んでいたものを、
npmのライブラリを通してimportして書く方式に少しづつ修正する方針です。

www.npmjs.com

検品して梱包する男性

*1:デフォルトでは"node_modules"が設定されているため、再定義する場合は"node_modules"も合わせて記述する必要があります。

*2:特定の関数のみクリアする場合は mockFn.mockClear()を使います。

*3:当プロジェクトではRailsのERBからreactコンポーネントを呼び出しています。

丸山 礼 (記事一覧)

サービス開発課でCloud Automatorを開発しています。