進化したJestのスナップショット機能でReactコンポーネントを効率よくテストする

2016/10/31

Jack Franklin

0

Articles in this issue reproduced from SitePoint
Copyright © 2016, All rights reserved. SitePoint Pty Ltd. www.sitepoint.com. Translation copyright © 2016, KADOKAWA CorporationJapanese syndication rights arranged with SitePoint Pty Ltd, Collingwood, Victoria,Australia through Tuttle-Mori Agency, Inc., Tokyo

生まれ変わったFacebook製テストフレームワーク「Jest」とは何か?」に続き、テストフレームワーク「Jest」を使ったReactコンポーネントのテスト方法について解説します。特に、スナップショットを使ったテスト方法は必読です。

Reactコンポーネントをテストする

注目すべきは、デフォルトではReactコンポーネントにあまりたくさんテストを記述しすぎない方が良いことです。ビジネスロジックなどのかなり徹底的にテストしたいものは、前述のステート関数のテストのようにコンポーネントから引き出して独立した関数に置くべきです。

そうは言っても、Reactのインタラクションをテストするときには便利です(たとえばユーザーがボタンをクリックしたときに特定の関数が正しい引数で呼び出されるか確認するなど)。Reactコンポーネントが正しいデータをレンダリングしているかテストすることから始め、そのあとテストのインタラクションを調べます。その次に、Reactコンポーネントの出力をテストする、はるかに便利なJestの機能スナップショットへ進みます。

Reactコンポーネントが正しいデータをレンダリングしているかテストするには、Reactをテストする関数を提供するライブラリーreact-addons-test-utilsを使います。また、Airbnbによって書かれた、Reactコンポーネントのテストをとても簡単にするラッパーライブラリーEnzymeもインストールします。このAPIをテスト全体で使用します。Enzymeはすばらしいライブラリーで、ReactチームがReactコンポーネントをテストする方法として勧めています。

npm install --save-dev react-addons-test-utils enzyme

Todoコンポーネントがパラグラフ内のtodoのテキストをレンダリングするかテストします。__tests__/todo.test.jsを作成し、コンポーネントをインポートします。

import Todo from '../app/todo';
import React from 'react';
import { mount } from 'enzyme';

test('Todo component renders the text of the todo', () => {
});

また、Enzymeからもmountをインポートします。mount関数はコンポーネントのレンダリングに使用され、出力の検査やアサーションの作成ができます。Nodeでテストを実行しながらDOMを必要とするテストを書くこともできます。これはJestがDOMをNodeに実装するライブラリーjsdomを構成しているからです。テストのたびにブラウザーを立ち上げることなくDOMベースのテストを書けるのはすばらしいことです。

Todoの作成にはmountを使用できます。

const todo = { id: 1, done: false, name: 'Buy Milk' };
const wrapper = mount(
  <Todo todo={todo} />
);

そして、Todoのテキストを含むパラグラフを見つけるためにwrapper.findを呼び出してCSSセレクターに渡します。このAPIのデザインは、jQueryを思い出させるかもしれません。これは、一致する要素を見つけるためにレンダリングされた出力を検索する、非常に直感的なAPIです。

const p = wrapper.find('.toggle-todo');

最後にBuy Milk内のテキストをアサートします。

expect(p.text()).toBe('Buy Milk');

テスト全体は次のようになります。

import Todo from '../app/todo';
import React from 'react';
import { mount } from 'enzyme';

test('TodoComponent renders the text inside it', () => {
  const todo = { id: 1, done: false, name: 'Buy Milk' };
  const wrapper = mount(
    <Todo todo={todo} />
  );
  const p = wrapper.find('.toggle-todo');
  expect(p.text()).toBe('Buy Milk');
});

「Buy Milk」が画面に配置されるのを確認するために作業が多く労力がたくさんかかると思うかもしれません。確かにそのとおりです。でも少し待ってください。次のセクションでは、これをもっと簡単にする機能を持つJestのスナップショットの使い方を取り上げます。

関数が特定の引数で呼び出されていることをアサートするために、Jestのスパイ機能をどう使うか説明します。Todoコンポーネントには、ユーザーがボタンをクリックするかインタラクションを実行するときに呼び出すプロパティとして、2つの関数が与えられています。そのため、今回はこのスパイ機能が便利です。

本テストでは、todoがクリックされたとき、コンポーネントが与えられているdoneChangeプロパティを呼び出すことをアサートします。

test('Todo calls doneChange when todo is clicked', () => {
});

呼び出しを追跡できる関数と、同時に呼び出される引数を置きたいとします。そして、ユーザーがtodoをクリックするとdoneChange関数と正しい引数が呼び出されていることを確認します。ありがたいことにJestにはスパイが備わっています。スパイの実装については気にしなくても構いません。いつどのように呼び出されるかは気をつけてください。関数をスパイしていると考えてください。作成には次のようにjest.fn()を呼び出します。

const doneChange = jest.fn();

これは、スパイでできており正しく呼び出されているか確認できる機能を提供しています。それでは、正しいプロパティでTodoをレンダリングします。

const todo = { id: 1, done: false, name: 'Buy Milk' };
const doneChange = jest.fn();
const wrapper = mount(
  <Todo todo={todo} doneChange={doneChange} />
);

次に、前述のテストと同じように、もう一度パラグラフを探します。

const p = TestUtils.findRenderedDOMComponentWithClass(rendered, 'toggle-todo');

それから、clickを引数として渡すユーザーイベントをシミュレーションするためにsimulateを呼び出します。

p.simulate('click');

あとはスパイ関数が正しく呼び出されていることをアサートするだけです。今回の場合はtodoのIDである1とともに呼び出されることを期待しています。これをアサートするためexpect(doneChange).toBeCalledWith(1)を使用します。それでテストは完了です。

test('TodoComponent calls doneChange when todo is clicked', () => {
  const todo = { id: 1, done: false, name: 'Buy Milk' };
  const doneChange = jest.fn();
  const wrapper = mount(
    <Todo todo={todo} doneChange={doneChange} />
  );

  const p = wrapper.find('.toggle-todo');
  p.simulate('click');
  expect(doneChange).toBeCalledWith(1);
});

スナップショットを使用した優れたコンポーネントテスト

Reactコンポーネントのテスト、特に(テキストレンダリングなどの)ありふれた機能の作業が多いと感じる可能性があることは、先に記しました。Jestでは、Reactコンポーネントで大量のアサーションをするのではなくスナップショットテストを実行します。これはインタラクションには不便ですが(その場合は上のようなテストをお勧めしますが)、コンポーネントの出力が正しいかテストするときには、とても簡単です。

スナップショットテストを実行するとき、JestはReactコンポーネントをテスト下でレンダリングし、結果をJSONファイルに保存します。テスト実行のたびに、JestはReactコンポーネントがスナップショットと同じ出力をレンダリングをしているか確認します。コンポーネントの動作を変更すると、Jestが次のどちらかを知らせます。

  • 間違いがあると気づき、コンポーネントを修正すれば再びスナップショットと適合
  • 変更は意図的なものなので、Jestにスナップショットを更新

この方法でのテストは以下のような意味になります。

  • Reactコンポーネントが思いどおりに動作しているか確認するためにたくさんのアサーションを書く必要はない
  • Jestが感知するので、コンポーネントの動作が誤って変更されてしまうことはない

また、すべてのコンポーネントのスナップショットを作成する必要はなく、作成しないことを強く勧めます。動作確認する必要がある機能とコンポーネントのみを選択してください。すべてのコンポーネントのスナップショットを取ると、テストの速度が遅くなるだけで不便です。Reactは徹底的してテストされたフレームワークなので、期待どおりに動作すると確信を持って構いません。コードではなくフレームワークをテストする羽目にならないよう確かめてください。

スナップショットのテストを開始するには、もう1つNodeパッケージが必要です。react-test-rendererは、Reactコンポーネントを受け取って純粋なJavaScriptオブジェクトとしてレンダリングできるパッケージです。これはファイルに保存されることを意味し、Jestがスナップショットを追跡し続けるために使用するものです。

npm install --save-dev react-test-renderer

では、スナップショットを使用するために最初のTodoコンポーネントを書き直します。差し当たりはTodoComponent calls doneChange when todo is clickedテストもコメントアウトしてください。

はじめにreact-test-rendererをインポートし、mountのインポートを削除します。両方を同時に使うことはできません。どちらか1つのみ使用します。テストをコメントアウトしたのは、これが理由です。

import renderer from 'react-test-renderer';

今度はレンダリングのためにインポートしたレンダラーを使い、スナップショットに適合していることをアサートします。

describe('Todo component renders the todo correctly', () => {
  it('renders correctly', () => {
    const todo = { id: 1, done: false, name: 'Buy Milk' };
    const rendered = renderer.create(
      <Todo todo={todo} />
    );
    expect(rendered.toJSON()).toMatchSnapshot();
  });
});

最初の実行で、Jestはこのコンポーネントにはスナップショットが存在しないことを認識し、作成します。__tests__/snapshots/todo.test.js.snapを調べます。

exports[`Todo component renders the todo correctly renders correctly 1`] = `
<div
  className="todo  todo-1">
  <p
    className="toggle-todo"
    onClick={[Function]}>
    Buy Milk
  </p>
  <a
    className="delete-todo"
    href="#"
    onClick={[Function]}>
    Delete
  </a>
</div>
`;

Jestが出力を保存し、今後またこのテストを実行するときに出力が同じかどうか確認します。これを実証するために、todoのテキストをレンダリングするパラグラフを削除することでコンポーネントを壊してみます。つまり、以下の行をTodoコンポーネントから削除します。

<p className="toggle-todo" onClick={() => this.toggleDone() }>{ todo.name }</p>

Jestは次のように反応します。

FAIL  __tests__/todo.test.js
 ● Todo component renders the todo correctly › renders correctly

   expect(value).toMatchSnapshot()

   Received value does not match stored snapshot 1.

   - Snapshot
   + Received

     <div
       className="todo  todo-1">
   -   <p
   -     className="toggle-todo"
   -     onClick={[Function]}>
   -     Buy Milk
   -   </p>
       <a
         className="delete-todo"
         href="#"
         onClick={[Function]}>
         Delete
       </a>
     </div>

     at Object.<anonymous> (__tests__/todo.test.js:21:31)
     at process._tickCallback (internal/process/next_tick.js:103:7)

Jestはスナップショットが新しいコンポーネントと適合しないことに気がつくと、出力して知らせます。この変更が正しいと思われる場合は、Jestを-uフラグで実行してスナップショットを更新します。今回の場合は変更を取り消すことで問題がなくなりました。

次に、インタラクションのテストにスナップショットのテストを使用する方法について取り上げます。テストごとに複数のスナップショットを持てるので、インタラクションが期待どおりかどうか出力をテストできます。

それらはステートをコントロールせず、与えられたコールバックプロパティを呼び出すので、TodoコンポーネントのインタラクションをJestのスナップショットでテストすることはできません。ここではスナップショットのテストを新しいファイルtodo.snapshot.test.jsへ移動し、トグルテストは todo.test.jsに残しています。これは、スナップショットのテストを異なるファイルに分離するのに便利だと分かりました。また、react-test-rendererreact-addons-test-utilsの間のコンフリクトを受け取らないことを意味しています。

記事に書いたコードはすべてGitHubにあるので、ローカルで実行してみてください。

最後に

FacebookがJestをリリースしたのはずいぶん前のことですが、ここ最近急激に取り上げられ開発が進んでいます。JavaScript開発者にどんどん好まれるようになり、改善されています。これまでフレームワークとはまったく異なるので、以前Jestを試したときは好みでなかった場合でも、ぜひもう一度試してみてください。高速で、再実行のスペックがすばらしく、エラーメッセージが的確で、さらにはスナップショットの機能が最高です。

※本記事は、ゲストライターのJack Franklinによるものです。SitePointのゲスト投稿では、JavaScriptコミュニティの著名な執筆者や講演者の魅力的なコンテンツの提供を目指しています。本記事はDan PrinceChristoph Pojerが査読を担当しています。最高のコンテンツに仕上げるために尽力してくれたSitePointの査読担当者のみなさんに感謝します。

(原文:How to Test React Components Using Jest

[翻訳:柴田理恵/編集:Livit

Copyright © 2016, Jack Franklin All Rights Reserved.

Jack Franklin

Jack Franklin

ロンドンで働くJavaScriptとRubyの開発者です。ツール作成、ES2015、ReactJSに重点的に取り組んでいます。

Loading...