ReactコンポーネントがUIテストを容易にする方法

公開: 2022-03-11

バックエンドのテストは簡単です。 選択した言語を選択し、それをお気に入りのフレームワークと組み合わせて、いくつかのテストを作成し、「実行」をクリックします。 コンソールに「Yay! できます!" 継続的インテグレーションサービスは、プッシュするたびにテストを実行します。人生は素晴らしいものです。

確かに、テスト駆動開発(TDD)は最初は奇妙ですが、予測可能な環境、複数のテストランナー、フレームワークに組み込まれたテストツール、継続的インテグレーションのサポートにより、作業が楽になります。 5年前、私はテストが私が今までに経験したすべての問題の解決策であると思いました。

その後、バックボーンが大きくなりました。

私たちは皆、フロントエンドMVCに切り替えました。 テスト可能なバックエンドは、栄光のデータベースサーバーになりました。 最も複雑なコードがブラウザに移動しました。 そして、私たちのアプリは実際にはもはやテスト可能ではありませんでした。

これは、フロントエンドコードとUIコンポーネントのテストが少し難しいためです。

モデルが正常に動作することを確認するだけであれば、それほど悪くはありません。 または、関数を呼び出すと正しい値が変更されます。 Reactユニットテストのために必要なことは次のとおりです。

  • 整形式の分離されたモジュールを作成します。
  • 関数を実行するには、JasmineまたはMochaテスト(またはその他)を使用します。
  • KarmaやChutzpahなどのテストランナーを使用します。

それでおしまい。 私たちのコードはユニットテストされています。

以前は、フロントエンドテストの実行が難しい部分でした。 すべてのフレームワークには独自のアイデアがあり、ほとんどの場合、テストを実行するたびに手動で更新するブラウザーウィンドウが表示されます。 もちろん、あなたはいつも忘れるでしょう。 少なくとも、私はそうしました。

2012年、Vojta JinaはKarmaランナー(当時はTestacularと呼ばれていました)をリリースしました。 Karmaを使用すると、フロントエンドテストはツールチェーンの完全な市民になります。 Reactテストは、ターミナルまたは継続的インテグレーションサーバーで実行され、ファイルを変更すると自動的に再実行されます。また、複数のブラウザーで同時にコードをテストすることもできます。

これ以上何を望むことができますか? さて、実際にフロントエンドコードをテストします。

フロントエンドテストには、単体テスト以上のものが必要です

単体テストは素晴らしいです。アルゴリズムが毎回正しいことを行うかどうかを確認したり、入力検証ロジック、データ変換、またはその他の分離された操作をチェックしたりするのに最適な方法です。 ユニットテストはファンダメンタルズに最適です。

しかし、フロントエンドコードは、データを操作することではありません。 それは、ユーザーイベントと、適切なタイミングで適切なビューをレンダリングすることです。 フロントエンドはユーザーに関するものです。

これが私たちができることをしたいことです:

  • Reactユーザーイベントをテストする
  • それらのイベントへの応答をテストします
  • 適切なものが適切なタイミングでレンダリングされることを確認してください
  • 多くのブラウザでテストを実行する
  • ファイルの変更に対してテストを再実行します
  • Travisのような継続的インテグレーションシステムと連携する

私がこれを行ってきた10年間、Reactを突っ込み始めるまで、ユーザーインタラクションとビューレンダリングをテストするための適切な方法を見つけられませんでした。

Reactユニットテスト:UIコンポーネント

Reactは、これらの目標を達成するための最も簡単な方法です。 一部には、テスト可能なパターンを使用してアプリを設計する必要があるため、一部には、素晴らしいReactテストユーティリティがあるためです。

これまでにReactを使用したことがない場合は、私の本React+d3.jsをチェックしてください。 それは視覚化を対象としていますが、Reactの「素晴らしい軽量のイントロ」だと言われています。

Reactは、すべてを「コンポーネント」として構築することを強制します。 Reactコンポーネントは、ウィジェット、またはいくつかのロジックを備えたHTMLのチャンクと考えることができます。 それらは、オブジェクトであることを除いて、関数型プログラミングの最良の原則の多くに従います。

たとえば、同じパラメーターセットが与えられた場合、Reactコンポーネントは常に同じ出力をレンダリングします。 何度レンダリングしても、誰がレンダリングしても、出力をどこに配置しても。 いつも同じ。 その結果、Reactコンポーネントをテストするために複雑なスキャフォールディングを実行する必要はありません。 それらはプロパティのみを考慮し、グローバル変数と構成オブジェクトの追跡は必要ありません。

これは、状態を回避することで大部分が達成されます。 関数型プログラミングでは、これを参照の透明性と呼びます。 Reactにはこれに特別な名前はないと思いますが、公式ドキュメントでは、状態の使用をできるだけ避けることを推奨しています。

ユーザーインタラクションのテストに関しては、Reactは関数コールバックにバインドされたイベントでカバーしています。 テストスパイを設定し、クリックイベントが適切な関数を呼び出すことを確認するのは簡単です。 また、Reactコンポーネントはそれ自体をレンダリングするため、クリックイベントをトリガーして、HTMLの変更を確認するだけです。 これは、Reactコンポーネントがそれ自体のみを気にするために機能します。 ここをクリックしても、変更はありませ。 明確に定義された関数呼び出しだけで、イベントハンドラーのネストを処理する必要はありません。

ああ、Reactは魔法なので、DOMについて心配する必要はありません。 Reactは、いわゆる仮想DOMを使用して、コンポーネントをJavaScript変数にレンダリングします。 そして、仮想DOMへの参照は、実際にReactコンポーネントをテストするために必要なすべてです。

かなり甘いです。

ReactのTestUtils

Reactには一連の組み込みTestUtilsが付属しています。 Jestというおすすめのテストランナーもいますが、私は好きではありません。 その理由を少し説明します。 まず、 TestUtilsです。

require('react/addons').addons.TestUtilsようなことをすることでそれらを取得します。 これは、ユーザーの操作をテストし、出力を確認するためのエントリポイントです。

React TestUtilsを使用すると、ページに挿入する代わりに、DOMを変数に入れることでReactコンポーネントをレンダリングできます。 たとえば、Reactコンポーネントをレンダリングするには、次のようにします。

 var component = TestUtils.renderIntoDocument( <MyComponent /> );

次に、 TestUtilsを使用して、すべての子がレンダリングされたかどうかを確認できます。 このようなもの:

 var h1 = TestUtils.findRenderedDOMComponentWithTag( component, 'h1' );

findRenderedDOMComponentWithTagは、そのように聞こえます。子を調べ、探しているコンポーネントを見つけて、それを返します。 戻り値はReactコンポーネントのように動作します。

次に、 getDOMNode()を使用して生のDOM要素にアクセスし、その値をテストできます。 コンポーネントのh1タグに「タイトル」と表示されていることを確認するには、次のように記述します。

 expect(h1.getDOMNode().textContent) .toEqual("A title");

まとめると、完全なテストは次のようになります。

 it("renders an h1", function () { var component = TestUtils.renderIntoDocument( <MyComponent /> ); var h1 = TestUtils.findRenderedDOMComponentWithTag( component, 'h1' ); expect(h1.getDOMNode().textContent) .toEqual("A title"); });

すばらしい点は、TestUtilsを使用するとユーザーイベントもトリガーできることです。 クリックイベントの場合、次のように記述します。

 var node = component .findRenderedDOMComponentWithTag('button') .getDOMNode(); TestUtils.Simulate.click(node);

これはクリックをシミュレートし、潜在的なリスナーをトリガーします。リスナーは、出力、状態、またはその両方を変更するコンポーネントメソッドである必要があります。 これらのリスナーは、必要に応じて親コンポーネントの関数を呼び出すことができます。

すべてのケースをテストするのは簡単です。変更された状態はcomponent.stateにあり、通常のDOM関数を使用して出力にアクセスでき、スパイを使用して関数呼び出しを行うことができます。

なぜジェストではないのですか?

Reactの公式ドキュメントでは、https://facebook.github.io/jest/をテストランナーおよびReactテストフレームワークとして使用することを推奨しています。 JestはJasmineに基づいて構築されており、同じ構文を使用します。 Jasmineから得られるすべてのものに加えて、Jestはテストしているコンポーネントを除くすべてをモックします。 これは理論的には素晴らしいですが、私はそれが面倒だと思います。 まだ実装していないもの、またはコードベースの別の部分からのものは、 undefinedです。 これは多くの場合問題ありませんが、静かに失敗するバグにつながる可能性があります。

たとえば、クリックイベントのテストに問題がありました。 私が何を試しても、リスナーを呼び出さないだけです。 それから私は関数がジェストによって嘲笑されたことに気づきました、そしてそれは私にこれを決して教えませんでした。

しかし、Jestの最悪の犯罪は、これまで、新しい変更を自動的にテストするための監視モードがなかったことです。 一度実行してテスト結果を取得することができ、それだけです。 (私は仕事中にバックグラウンドでテストを実行するのが好きです。そうでなければ、テストを実行するのを忘れます。)現在、これはもはや問題ではありません。

ああ、Jestは複数のブラウザーでのReactテストの実行をサポートしていません。 これは以前ほど問題ではありませんが、まれに、特定のバージョンのChromeでのみ特異なバグが発生する重要な機能だと思います…

編集者注:この記事が最初に書かれて以来、Jestは大幅に改善されました。 最近のチュートリアル「酵素とJestを使用したReactユニットテスト」を読んで、Jestテストが今日のタスクに対応しているかどうかを自分で判断できます。

Reactテスト:統合された例

とにかく、理論的には、優れたフロントエンドReactテストがどのように機能するかを見てきました。 簡単な例でそれを実行に移しましょう。

Reactとd3.jsで作成された散布図コンポーネントを使用して乱数を生成するさまざまな方法を視覚化します。 コードとそのデモもGithubにあります。

Karmaをテストランナーとして、Mochaをテストフレームワークとして、Webpackをモジュールローダーとして使用します。

セットアップ

ソースファイルは<root>/srcディレクトリに配置され、テストは<root>/src/__tests__ディレクトリに配置されます。 src内に、主要コンポーネントごとに1つずつ、それぞれ独自のテストファイルを持つ複数のディレクトリを配置できるという考え方です。 このようにソースコードとテストファイルをバンドルすると、さまざまなプロジェクトでReactコンポーネントを簡単に再利用できます。

ディレクトリ構造が整ったら、次のような依存関係をインストールできます。

 $ npm install --save-dev react d3 webpack babel-loader karma karma-cli karma-mocha karma-webpack expect

インストールに失敗した場合は、インストールのその部分を再実行してみてください。 NPMは、再実行時に消える方法で失敗することがあります。

完了すると、 package.jsonファイルは次のようになります。

 // package.json { "name": "react-testing-example", "description": "A sample project to investigate testing options with ReactJS", "scripts": { "test": "karma start" }, // ... "homepage": "https://github.com/Swizec/react-testing-example", "devDependencies": { "babel-core": "^5.2.17", "babel-loader": "^5.0.0", "d3": "^3.5.5", "expect": "^1.6.0", "jsx-loader": "^0.13.2", "karma": "^0.12.31", "karma-chrome-launcher": "^0.1.10", "karma-cli": "0.0.4", "karma-mocha": "^0.1.10", "karma-sourcemap-loader": "^0.3.4", "karma-webpack": "^1.5.1", "mocha": "^2.2.4", "react": "^0.13.3", "react-hot-loader": "^1.2.7", "react-tools": "^0.13.3", "webpack": "^1.9.4", "webpack-dev-server": "^1.8.2" } }

いくつかの構成の後、 npm test karma startいずれかを使用してテストを実行できるようになります。

テストの実行

構成

構成にはあまり意味がありません。 Webpackがコードを見つける方法を知っていること、およびKarmaがテストの実行方法を知っていることを確認する必要があります。

./tests.webpack.jsファイルに2行のJavaScriptを入れて、KarmaとWebpackが一緒にプレイできるようにします。

 // tests.webpack.js var context = require.context('./src', true, /-test\.jsx?$/); context.keys().forEach(context);

これにより、Webpackは、 -testサフィックスが付いたものをすべてテストスイートの一部と見なすようになります。

Karmaの構成には、もう少し作業が必要です。

 // karma.conf.js var webpack = require('webpack'); module.exports = function (config) { config.set({ browsers: ['Chrome'], singleRun: true, frameworks: ['mocha'], files: [ 'tests.webpack.js' ], preprocessors: { 'tests.webpack.js': ['webpack'] }, reporters: ['dots'], webpack: { module: { loaders: [ {test: /\.jsx?$/, exclude: /node_modules/, loader: 'babel-loader'} ] }, watch: true }, webpackServer: { noInfo: true } }); };

これらの行のほとんどは、デフォルトのKarma構成からのものです。 browsersを使用して、テストはChromeで実行する必要があると言い、 frameworksワークは使用するテストフレームワークを指定し、 singleRunはデフォルトでテストを1回だけ実行するようにしました。 karma start --no-single-runを使用して、カルマをバックグラウンドで実行し続けることができます。

これらの3つは明らかです。 Webpackのものはもっと面白いです。

Webpackはコードの依存関係ツリーを処理するため、 files配列内のすべてのファイルを指定する必要はありません。 必要なのはtests.webpack.jsだけで、必要なすべてのファイルが必要です。

webpack設定を使用して、Webpackに何をすべきかを指示します。 通常の環境では、この部分はwebpack.config.jsファイルに含まれます。

また、JavaScriptにbabel-loaderを使用するようにWebpackに指示します。 これにより、ECMAScript2015ReactのJSXからのすべての素晴らしい新機能が提供されます。

webpackServer構成では、デバッグ情報を出力しないようにWebpackに指示します。 それは私たちのテスト出力を台無しにするだけです。

Reactコンポーネントとテスト

テストスイートを実行すると、残りは簡単です。 ランダムな座標の配列を受け入れ、多数のポイントを持つ<svg>要素を作成するコンポーネントを作成する必要があります。

Reactテストのベストプラクティス(つまり、標準のTDDプラクティス)に従って、最初にテストを作成し、次に実際のReactコンポーネントを作成します。 src/__tests__/のバニラテストファイルから始めましょう:

 // ScatterPlot-test.jsx var React = require('react/addons'), TestUtils = React.addons.TestUtils, expect = require('expect'), ScatterPlot = require('../ScatterPlot.jsx'); var d3 = require('d3'); describe('ScatterPlot', function () { var normal = d3.random.normal(1, 1), mockData = d3.range(5).map(function () { return {x: normal(), y: normal()}; }); });

まず、React、そのTestUtils、d3.js、 expectライブラリ、およびテストするコードが必要です。 次に、 describeを使用して新しいテストスイートを作成し、ランダムデータを作成します。

最初のテストでは、 ScatterPlotがタイトルをレンダリングすることを確認しましょう。 私たちのテストはdescribeブロック内に入ります:

 // ScatterPlot-test.jsx it("renders an h1", function () { var scatterplot = TestUtils.renderIntoDocument( <ScatterPlot /> ); var h1 = TestUtils.findRenderedDOMComponentWithTag( scatterplot, 'h1' ); expect(h1.getDOMNode().textContent).toEqual("This is a random scatterplot"); });

ほとんどのテストは同じパターンに従います。

  1. 与える。
  2. 特定のノードを検索します。
  3. 内容を確認してください。

前に見たように、 renderIntoDocumentはコンポーネントをレンダリングし、 getDOMNode findRenderedDOMComponentWithTag生のDOMアクセスを提供します。

最初、私たちのテストは失敗します。 合格するには、タイトルタグをレンダリングするコンポーネントを作成する必要があります。

 var React = require('react/addons'); var d3 = require('d3'); var ScatterPlot = React.createClass({ render: function () { return ( <div> <h1>This is a random scatterplot</h1> </div> ); } }); module.exports = ScatterPlot;

それでおしまい。 ScatterPlotコンポーネントは、期待されるテキストを含む<h1>タグを持つ<div>をレンダリングし、テストに合格します。 はい、それは単なるHTMLよりも長いですが、我慢してください。

残りのフクロウを描く

上記のように、残りの例はGitHubで見ることができます。 この記事では段階的な説明は省略しますが、一般的なプロセスは上記と同じです。 ただし、もっと興味深いテストをお見せしたいと思います。 すべてのデータポイントがグラフに表示されることを確認するテスト:

 // ScatterPlot-test.jsx it("renders a circle for each datapoint", function () { var scatterplot = TestUtils.renderIntoDocument( <ScatterPlot data={mockData} /> ); var circles = TestUtils.scryRenderedDOMComponentsWithTag( scatterplot, 'circle' ); expect(circles.length).toEqual(5); });

以前と同じ。 レンダリング、ノードの検索、結果の確認。 ここで興味深いのは、これらのDOMノードを描画することです。 次のように、d3.jsの魔法をScatterPlotコンポーネントに追加します。

 // ScatterPlot.jsx componentWillMount: function () { this.yScale = d3.scale.linear(); this.xScale = d3.scale.linear(); this.update_d3(this.props); }, componentWillReceiveProps: function (newProps) { this.update_d3(newProps); }, update_d3: function (props) { this.yScale .domain([d3.min(props.data, function (d) { return dy; }), d3.max(props.data, function (d) { return dy; })]) .range([props.point_r, Number(props.height-props.point_r)]); this.xScale .domain([d3.min(props.data, function (d) { return dx; }), d3.max(props.data, function (d) { return dx; })]) .range([props.point_r, Number(props.width-props.point_r)]); }, ...

componentWillMountを使用してXドメインとYドメインの空のd3スケールを設定し、 componentWillReceivePropsを使用して何かが変更されたときに更新されるようにします。 次に、 update_d3は、両方のスケールのdomainrangeを設定していることを確認します。

2つのスケールを使用して、データセット内のランダムな値と画像上の位置の間を変換します。 ほとんどのランダムジェネレーターは、 [0,1]の範囲の数値を返します。これは、ピクセルとして表示するには小さすぎます。

次に、コンポーネントのレンダリングメソッドにポイントを追加します。

 // ScatterPlot.jsx render: function () { return ( <div> <h1>This is a random scatterplot</h1> <svg width={this.props.width} height={this.props.height}> {this.props.data.map(function (pos, i) { var key = "circle-"+i; return ( <circle key={key} cx={this.xScale(pos.x)} cy={this.yScale(pos.y)} r={this.props.point_r} /> ); }.bind(this))}; </svg> </div> ); }

このコードはthis.props.data配列を調べ、各データポイントに<circle>要素を追加します。 単純。

Reactコンポーネントのテストで、UIテストに絶望することはもうありません。
つぶやき

Reactとd3.jsを組み合わせてデータ視覚化コンポーネントを作成する方法について詳しく知りたい場合は、私の本React+d3.jsをチェックするもう1つの大きな理由です。

自動化されたReactコンポーネントテスト:思ったより簡単

Reactを使用してテスト可能なフロントエンドコンポーネントを作成するために知っておく必要があるのはこれだけです。 Reactコンポーネントをテストするその他のコードを確認するには、前述のように、GithubでReactテストのサンプルコードベースを確認してください。

私たちはそれを学びました:

  1. Reactは、モジュール化とカプセル化を強制します。
  2. これにより、ReactUIテストを簡単に自動化できます。
  3. フロントエンドには単体テストだけでは不十分です。
  4. カルマは素晴らしいテストランナーです。
  5. ジェストには可能性がありますが、まだそこにはありません。 (または多分今です。)

この記事が気に入ったら、Twitterでフォローして、下にコメントを残してください。 読んでくれてありがとう、そして幸せなReactテスト!

関連: Reactのパフォーマンスを向上させるためにコンポーネントを最適化する方法