前回の記事はこちら。
前回で、JestとReactの環境構築が終わった。
以降は、個人的に特に習得したいと思っている、イベントとDOMのテストに取り組む。
今回は、イベントのテストについて。
クリックイベントの挙動をテストする
Reactでの開発に限らず、ウェブアプリケーションにおいては、ユーザーによるクリックをトリガーにしてイベントを起こす、というのが一般的だと思われる。
クリックによって何かが送信されたり、表示が切り替わったり。
そのため、クリックイベントの挙動を確認できないと、テストとしては不十分なものになってしまう。
Jestでは、enzyme
というツールと組み合わせることで、Reactのコンポーネントにセットしたイベントを簡単にテストできる。
airbnb/enzyme: JavaScript Testing utilities for React
まずは、テストの対象となるReactのコードを書く。
これを、click.js
とする。
// click.js import React from 'react'; import ReactDOM from 'react-dom'; class App extends React.Component{ constructor(props){ super(props); }; render(){ const {event} = this.props; return( <div> <span onClick={event}>Click!</span> </div> ); }; }; function sampleFunc(){ alert('clickされました。'); }; ReactDOM.render(<App event={sampleFunc} />, document.getElementById('mount'));
このコードをビルドしてhtmlファイルで読み込むと、画面にはClick!
と表示され、そこをクリックするとダイアログボックスが表示される。
App
コンポーネントを使う際にevent
属性に渡した関数(上記ではsampleFunc()
)が、span
要素のクリックイベントとして設定される。という仕組み。
これをテストしていく。
元となるコードを修正する
早速テストを書いていく。と思っていたのだが、エラーが発生し、修正を余儀なくされた。
元となるコードにReactDOM.render()
があるとエラーになることが分かった。
このメソッドが悪いというより、ブラウザ環境以外では引数のdocument.getElementById('mount')
が実行できなくてエラーになる、ということだと思う。
ということで、元ファイルを書き換えた。
// click.js import React from 'react'; class App extends React.Component{ constructor(props){ super(props); }; render(){ const {event} = this.props; return( <div> <span onClick={event}>Click!</span> </div> ); }; }; function sampleFunc(){ alert('clickされました。'); }; export { App, sampleFunc };
// click-render.js import React from 'react'; import ReactDOM from 'react-dom'; import {App, sampleFunc} from './click.js'; ReactDOM.render(<App event={sampleFunc} />, document.getElementById('mount'));
このように2つのファイルに分割し、click-render.js
をビルドしたものをhtmlファイルで読み込むようにした。
そうすることで、引き続きブラウザ上で問題なく動作し、かつ、click.js
の内容をテストできるようになった。
必要なnpmパッケージをインストールする
先述のようにenzyme
というツールを使うので、インストールする。
同時に、react-addons-test-utils
もインストールしておく。
npm install -S enzyme react-addons-test-utils
react-addons-test-utils
はテストで直接使うことはないが、これをインストールしておかないと、テスト実行時に以下のエラーが出る。
enzyme
を使わないテストなら問題ないようだが、使う際には必要となる。
react-addons-test-utils is an implicit dependency in order to support react@0.13-14. Please add the appropriate version to your devDependencies.
テストを書く
やっと準備が整ったので、テストを書く。
// click.test.js import React from 'react'; import {shallow} from 'enzyme'; import {App} from '../click.js'; describe('clickイベントのテスト', ()=>{ test('Appコンポーネントにイベントを渡す', ()=>{ const testMock = jest.fn(); const subject = shallow(<App event={testMock} />); subject.find('span').simulate('click'); expect(testMock).toHaveBeenCalled(); }); });
このテストを実行すると、無事にパスする。
jest.fn()
これは、関数のモックを作成している。
モック、というのはテストでよく使われる概念のようだが、よく分かっていない。
テスト用のダミーのデータ、というような意味だと思う。
今回のテストは、関数そのものの挙動ではなく、クリックで関数が呼ばれるかどうかを調べるのが、目的である。
そのため、関数そのものは何でもいい。
そういったときに、モックを使う。
今回の例ではtestMock
という関数のモックを、App
コンポーネントのevent
属性に渡している。
つまり、span
要素をクリックした際にtestMock
が実行されれば、テストは成功と言える。
toHaveBeenCalled()
関数が呼ばれたかどうかは、toHaveBeenCalled()
を使う。
expect(調べたい関数).toHaveBeenCalled();
このようにすると、調べたい関数が呼び出された場合のみ、パスする。呼び出されなかった場合、テストはパスしない。
shallow()
関数のモックを作り、それが呼び出されたかどうかを調べる仕組みは出来た。
後は、span
要素に対してクリックイベントを発生させるだけである。
そのために、enzyme
からインポートしたshallow
という関数を使う。
この関数を実行すると、引数として渡したReactのコンポーネントをレンダリングしたことになるようだ。
そしてその戻り値として、オブジェクトが返される。
このオブジェクトは様々なメソッドを使えるようだが、ここで重要なのは、find()
とsimulate()
の2つである。
shallowの戻り値.find(対象となる要素).simulate(イベント名);
このように書くことで、対象となる要素に対して、イベントを起こすことが出来る。
そのため、下記のコードは、App
コンポーネントのspan
要素に対してクリックイベントを起こす、という意味になる。
const subject = shallow(<App event={testMock} />); subject.find('span').simulate('click');
要素の特定は、#
や.
を使ってid
やclassName
に対して行うことも可能。
find()の注意点
find()
を使う際は、対象の要素が一つに絞れないといけない。対象となる要素が複数あると、エラーになる。
以下は、span
要素が複数あるため、エラーになる。
// コンポーネント <div> <span onClick={event}>aaaa</span> <span onClick={event}>aaaa</span> <span onClick={event}>aaaa</span> </div>
// テスト subject.find('span').simulate('click'); expect(testMock).toHaveBeenCalled();
// エラーメッセージ This method is only meant to be run on single node. 3 found instead.
以下は、要素を一つに絞れるため、問題ない。
<div> <span onClick={event}>aaaa</span> <span className="target" onClick={event}>aaaa</span> <span onClick={event}>aaaa</span> </div>
subject.find('.target').simulate('click'); expect(testMock).toHaveBeenCalled();
shallow()とmount()
ここまで書いてきたshallow()
は、mount()
と置き換えても問題なく動作する。mount()
でも、find()
やsumilate()
は使える。
両者の違いは、子コンポーネントを展開するかどうか。他にも違いはあるだろうが、今の自分にとって重要なのはこの点である。
mount()
は子コンポーネントを展開するが、shallow()
は展開しない。
その違いを示したのが、下記のテスト。
// click2.test.js import React from 'react' import { shallow, mount } from 'enzyme' class Sample extends React.Component { constructor(props) { super(props) }; render() { const {event} = this.props; return ( <div> <SampleChild event={event} /> </div> ) } } class SampleChild extends React.Component{ constructor(props) { super(props) }; render(){ const {event} = this.props; return( <span onClick={event} >aaa</span> ); }; }; describe('shallowとmountの違い', () => { test('shallowの場合', () => { const testMock = jest.fn(); const subject = shallow(<Sample event={testMock} />); subject.find('span').simulate('click'); expect(testMock).toHaveBeenCalled(); }); test('mountの場合', () => { const testMock = jest.fn(); const subject = mount(<Sample event={testMock} />); subject.find('span').simulate('click'); expect(testMock).toHaveBeenCalled(); }); })
shallowとmountの違い ✕ shallowの場合 (12ms) ✓ mountの場合 (19ms) Test Suites: 1 failed, 1 total Tests: 1 failed, 1 passed, 2 total Snapshots: 0 total Time: 1.782s, estimated 2s Ran all test suites matching "__tests__/click2.test.js".
Sample
コンポーネントはSampleChild
コンポーネントを包含しているという構造。
このような状態でSample
に対してshallow()
を実行すると、SampleChild
は無視される。
そのため、SampleChild
が持っているspan
要素は無視され、テストは通らない。
一方、mount()
では、SmapleChild
も展開されるため、テストはパスする。
これでイベントのテストは出来るようになった。
次回は、DOMのテストに取り組む。