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のグローバルオブジェクトに警告してしまうという問題がありました。
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.js
でmoduleDirectories
に
"../../"
(= プロジェクトルート)を設定するようにしました。*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を利用するコンポーネントをテストできる
という使い分けのようです。
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(); });
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して書く方式に少しづつ修正する方針です。
*1:デフォルトでは"node_modules"が設定されているため、再定義する場合は"node_modules"も合わせて記述する必要があります。
*2:特定の関数のみクリアする場合は mockFn.mockClear()を使います。
*3:当プロジェクトではRailsのERBからreactコンポーネントを呼び出しています。
丸山 礼 (記事一覧)
サービス開発課でCloud Automatorを開発しています。