react-create-app
で作成した React Hooks を使ったアプリケーションのテストのメモ
TL;DR
下書き途中にしたまま数ヶ月が経過してしまったので、少し情報が古くなってしまってるかもですが書きかけていた Jest + enzyme + act で React Hooks のテストをしてみたエントリーを揚げます🍤
Jest
Facebook 製 JavaScript のユニットテストツール群。
Snapshot Test などさまざまな単体テストを書くためのfunction群と、テストを実行するテストランナーが含まれている。
create-react-app
で作成したアプリにはデフォルトで入っている? (最新では ReactTestUtils
になってるかも )
Jest の特徴
多くのライブラリは テストランナー・アサーション・テストモック など機能別にライブラリが分かれていて必要に応じて組み合わせて使う必要があったが、Jest はこれひとつで基本的なテストができるの機能が含まれるので、ライブラリの依存などの問題がなく、テストをするためのライブラリ選定や環境作成の手間から開放される。
cf. Jestで始める! ユニットテスト - 環境の準備とテストの実行 | CodeGrid
it(testName, fn)
or test(testName, fn)
でテストスコープを作成して、expect()
で評価する
it('foo is 1', () => { expect(foo).toBe(1); }); it('bar is not 1', () => { expect(bar).not.toBe(1); });
Enzyme
Airbnb 製 React のテストユーティリティ。 React の公式ドキュメントでも仕様が推奨されていいて、React のテストは Jest + Enzyme で行うのが鉄板になっているっぽい。
Enzyme の特徴
テストするコンポーネントに紐付いた子コンポーネントなしにレンダリングする shallow
や、子コンポーネントを含んでレンダリングする mount
などの機能がある。
jQuery の影響を受けているようで jQuery のような書き方でセレクタ指定で JSX のDOMにアクセスできる API も用意されていて、直感的にテスト対象を指定できるようになる。 (querySelector
の指定方法も jQuery の影響受けてる気がする)
intsall
$ yarn add --dev enzyme
React v.16 系で使用する場合
Enzyme v2.x では、React v.16 のアプリで使うには設定する必要がある。 Enzyme v3 からは React v.16 がサポートされるらしい。
$ yarn add --dev enzyme-adapter-react-16
Settings
設定ファイルは別に用意しておき import するのが楽だと思います。
import { configure } from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; configure({ adapter: new Adapter() });
Debug
shallow
や mount
でレンダリングしているコンポーネントがどうなっているのか、debug()
で確認することができる。
const app = shallow(<App />);
console.log( app.debug() );
cf. debug() · Enzyme
act() from 'react-dom/test-utils'
act()
内で実行することで useEffect
Hook を含む結果をテストすることができる。
※ jest
, enzyme
だけだと effect
系の Hooks を含むテストが上手くできない。
act を使ったテスト
act 無しで effect のテストが落ちるパターン
App.js
// App.js import React, { useState, useEffect } from 'react'; function App() { const [count, setCount] = useState(0); useEffect(() => { document.title = `You clicked ${count} times.`; }); const countup = () => setCount(count + 1); return ( <div className="app"> <p>You clicked <span className="count">${count}</span> times.</p> <button onCLick={countup}>COUNT UP</button> </div> ); } export default App;
__test__/App.test.js
import React from 'react'; import './enzyme.setup.js'; // adapter for react 16 import { shallow, mount } from 'enzyme'; import App from '../App'; let container; beforeEach(() => { container = global.document.createElement('dev'); global.document.body.appendChaild(container); global.document.title = "React App"; }); afterEach(() => { global.document.body.removeChaild(container); container = null; }); describe('Button on click', () => { const app = mount(<App />, { attachTo: container }); app.find('button').simulate('click'); it('count is 1', () => { expect(app.find('.count').text()).toEqual("1"); }); it('Title is "You clicked 1 times."', () => { expect(document.title).toEqual('You clicked 1 times.'); // => Failed }); });
effect (副作用) である useEffect
の処理の実行が取れないので、document.title
を変更している Title is "You clicked 1 times.
のテストは落ちる
👇
$ yarn run test Button on click › Title is "You clicked 1 times." expect(received).toEqual(expected) // deep equality Expected: "You clicked 1 times." Received: "React App"
act (react-dom/test-utils) で使って effect のテストを通るようにする
act()
内でコンポーネントのレンダリングを行うことで、コンポーネントの effect (副作用) を含んでテストを行うことが出来る。
先の例は act()
を使って 次のように書き換えます。
import React from 'react'; import { act } from 'react-dom/test-utils'; import './enzyme.setup.js'; import { shallow, mount } from 'enzyme'; import App from '../App'; // 中略 describe('Button on click', () => { // act で実行される effect の状態は act() のある同スコープ内でのみ引き継がれる it('Count is 1 & Title become "You clicked 1 times."', () => { // act() 内でレンダリングするので let にする let app; // First Render with Effect act(() => { app = mount(<App />, { attachTo: container }); }); console.log( app.find('.count').text() ); // => 0 console.log( document.title ); // => You clicked 0 times. ... useEffect が実行されている // Second Render with Effect act(() => { app.find('button').simulate('click'); }); expect(app.find('.count').text()).toEqual("1"); expect(document.title).toEqual('You clicked 1 times.'); }); });
act()
に中で APP に対するアクションを実行することで useEffect
内の処理が取れるようになるので、document.title
を更新するテストも通るようになります。
act を使う時の注意点
act
を使ったテストは act()
を実行しているスコープ内でのみ引き継がれるので、act()
を実行している it
, describe
直下に expect()
を置くある必要あがある。
つまり複数の it
を使いたい場合は it
内で都度レンダリングから行う必要がある。
次のような書き方はeffect
の実行後の状態が引き継がれないので、Title is "You clicked 1 times.
のテストが落ちる
describe('on clicked', () => { let app; // First Render with Effect act(() => { app = mount(<App />, { attachTo: container }); }); console.log( app.find('.count').text() ); // => 0 console.log( document.title ); // => You clicked 0 times. // Second Render with Effect act(() => { app.find('button').simulate('click'); }); console.log( app.find('.count').text() ); // => 1 console.log( document.title ); // => You clicked 1 times. // ※ act() のあるスコープでは effect の結果が引き継がれている it('count is 1', () => { expect(app.find('.count').text()).toEqual("1"); }); it('Title is "You clicked 1 times.', () => { expect(document.title).toEqual('You clicked 1 times.'); // => "React App" // ※ 別スコープになると、effect の結果は引き継がれない }); });
👇 Run test
$ yarn run test on clicked ✓ count is 1 (3ms) ✕ Title is "You clicked 1 times. (2ms) ● on clicked › Title is "You clicked 1 times. expect(received).toEqual(expected) // deep equality Expected: "You clicked 1 times." Received: "React App"
先の effect のあるテストの describe
内で複数の it
を使いたい場合は次のようにする必要がある
describe('on clicked', () => { let app; it('count is 1', () => { app = shallow(<App />); app.find('button').simulate('click'); expect(app.find('.count').text()).toEqual("1"); }); it('Title is "You clicked 1 times.', () => { // First Render with Effect act(() => { app = mount(<App />, {attachTo: container}); console.log( document.title ); // => You clicked 0 times. // Second Render with Effect act(() => { app.find('button').simulate('click'); }); console.log( document.title ); // => You clicked 1 times. expect(document.title).toEqual('You clicked 1 times.'); app.unmount(); }); }); });
所感
テストをするには他にも Mock Functions とか色々と必要なこともありますが、とりあえす effect は jest だけではテストできなかったのでちょいハマりどころかもと思いました。
そして、いつの間にか React の公式ドキュメントの日本語ページが充実しててマジヤヴァイ。感謝の念しか無い (こんなエントリー読まなくても公式読めば解決すると思う。)
[参考]
- React v16.8: The One With Hooks – React Blog
- Running Tests · Create React App
- create-react-app + Jest + Enzyme で書くReactコンポーネントテストの始め方
- テストユーティリティ – React
- フロントエンドでTDDを実践する(react-testing-libraryを使った実践編) - Qiita
- React Testing Library · Testing Library
- A node.js tool to automate end-to-end web testing | TestCafe
Atomic Design ~堅牢で使いやすいUIを効率良く設計する
- 作者: 五藤佑典
- 出版社/メーカー: 技術評論社
- 発売日: 2018/04/25
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (1件) を見る
- 出版社/メーカー: サンエックス(San-x)
- メディア: おもちゃ&ホビー
- この商品を含むブログを見る