これは freee エンジニアによる、アドベントカレンダー 1日目の記事になります。言い出しっぺということで、初日を飾らせていただきました。
ビューのテストしてますか?
freee では、プロダクトによっていくつかの技術を組み合わせてフロントエンド開発を行っています。その中でES6 による React.js を使った実装が増えつつあります。特に、今の私の担当は、インタラクティブな挙動が求められ、DOMの状態が激しく変化する画面の開発となっております。そこで、React.js を使った開発が適していると判断し、かなり込み入ったReact.jsベースのビューコンポーネントを開発しています。
ところで、ビューのテストというと、下記のような様々な理由で相対的に優先度を低く扱われる場合が多いと思います。
- ユーザの操作が起点となるためテスト実装がしづらい
- 細かな変更頻度が高くテスト実装によるオーバーヘッドが重く感じる
- シナリオテストで吸収したくなるがシナリオ実装は負荷が高い
しかし、私の今回の担当分は複雑な実装が求められるために、きちんとテスト実装しないと今後のエンハンス効率を著しく損なうと考えられました。ですので、可能な限りのパターンでテストをしっかり実装することにしました。幸い、React.js はコンポーネントベースの設計思想が基になっており、テストもそのコンポーネント毎にわかりやすく実装することができます。
今回は、その ES6 on React.js のテストについて、私が経験したパターン毎の実装方法を紹介したいと思います。
サンプルアプリ
今回、説明のために用意したサンプルアプリは、GitHub上にアップしてあります。
下図のように、テキストボックスとテキスト表示エリアがあって、テキストボックスに値を入れると、それがそのままテキスト表示エリアに表示されるだけの、簡単なアプリです。
初期状態
テキスト入力後の状態
パターン
テストのパターンとして4つのパターンを紹介します。この4つのパターンを押さえておけば、大体の場面で、React.js のビューサイドのテストを高い網羅性でテストできるのではないでしょうか。
1. 出力DOMのテスト
Reactによって出力されるJSXのDOMのテストです。これには、expect-jsxが便利です。
下記のように、React Componentをレンダリングした際に、どのようなDOMが出力されるかを直観的に記述してテストすることができます。スタイルがどのように反映されるかもそのままテストできますので、非常に便利です。
import React from 'react';
import expect from 'expect';
import {createRenderer} from 'react-addons-test-utils';
import expectJSX from 'expect-jsx';
expect.extend(expectJSX);
import ViewComponent from '../src/view-component'
describe('ViewComponent', () => {
let renderer = createRenderer();
it('propsに設定した文字列がそのまま出力される', () => {
renderer.render(
<ViewComponent text="test" />
)
let actualElement = renderer.getRenderOutput();
let expectedElement = (
<div className="viewComponent">
test
</div>
);
expect(actualElement).toEqualJSX(expectedElement);
});
});
2. 子コンポーネント出力のテスト
今回のサンプルアプリでは、SampleComponent
から、InputComponent
とViewComponent
を呼び出す実装になっていますが、この子コンポーネント出力のテストについても、上述のように、expect-jsxが便利です。
it('子コンポーネントが正しく出力されることの確認', () => {
renderer.render(
<SampleComponent/>
)
let actualElement = renderer.getRenderOutput();
let expectedElement = (
<div className="sampleComponent">
<InputComponent
ref="iptCmp"
placeholder="input something ..."
inputChangeHandler={function noRefCheck() {}}
/>
<ViewComponent
ref="viewCmp"
text="(ここにテキストボックスの値が入ります)"/>
</div>
);
expect(actualElement).toEqualJSX(expectedElement);
});
ここで注目していただきたいのは、expectedElement
には、子コンポーネントのReactComponentがそのまま記述されている点です。つまり、子コンポーネント内部の処理は、このテストにおいてはまったく関係なく、あくまでもSampleComponent
単体のテストにクローズして記述できるということです。 これによって、テストの記述範囲と実際のテスト範囲を適切に絞り込んでテスト実装することができます。
3. state値のテスト
Componentの挙動は、親もしくは外部から与えられたpropsの値によって決まりますが、Component内部での状態変更については、stateで状態情報を管理し、利用します。このアプリでは、InputComponentの値をSampleComponentのstateで管理して、それをViewComponentのpropsに渡しています。このstate値のテストは以下のように記載します。
it('state初期値が正しく設定されていることの確認', () => {
const component = ReactTestUtils.renderIntoDocument(
<SampleComponent/>
);
expect(component.state.input_value).toBe('(ここにテキストボックスの値が入ります)');
});
一点注意していただきたいのは、これまでは、actualElement
を生成するためにgetRenderOutput
によって、ReactElement
を使っていましたが、ここでは、renderIntoDocument
によってReactComponent
を使っている点です。
4. イベント起点のテスト
最後に、Component内部でイベントを発生させて、それに伴う状態変更後の状態をテストする方法を紹介します。
パターン3と同様に、ReactComponentを生成して、それに対して、ReactTestUtils.Simulate
によってイベントを発生させ、それによって変化するstate値の値をテストしています。
it('テキストボックスに入力した値が正しくViewComponentに反映されることの確認', () => {
const component = ReactTestUtils.renderIntoDocument(
<SampleComponent/>
);
const ipt = component.refs['iptCmp'].refs['ipt'];
ReactTestUtils.Simulate.change(ipt, {target: {value: 'test'}});
expect(component.state.input_value).toBe('test');
});
おしまい
今回紹介した4つのパターンで、どのようにDOMが生成されるか、それをComponent単体範囲内でクローズさせて、Component内部の状態変更がどうなっているか、それをイベント契機でどう変化するかをテストできることが確認できました。
これだけのパターンが抑えられていれば、ビューとしての挙動のUnitテストをかなりの網羅性でテスト実装できるはずです。React.js はコンポーネントベースの開発がわかりやすくできるのが利点ですので、コンポーネント単位のテストをしっかりやって、再利用性をあげることは、React.jsを有効活用するにあたってとても重要なことです。
是非、テスト実装をしっかりやって、React.js を有効活用していきましょう。
明日は、弊社指折のフロントエンドヤンキーの @ymrl です!お楽しみに〜。
ご参考
サンプルアプリのソースは、GitHub上で確認できます。
宣伝
freee では、ES6 on React.js による 高度な UI を開発するフロントエンドエンジニアを募集しています。ご興味ある方はご応募ご連絡ください。