テスト駆動型React.js開発:酵素とJestを使用したReact.jsユニットテスト

公開: 2022-03-11

Michael Feathersによると、テストのないコードはすべてレガシーコードと呼ばれます。 したがって、レガシーコードの作成を回避する最良の方法の1つは、テスト駆動開発(TDD)を使用することです。

JavaScriptとReact.jsの単体テストに使用できるツールはたくさんありますが、この投稿では、JestとEnzymeを使用して、TDDを使用した基本機能を備えたReact.jsコンポーネントを作成します。

TDDを使用してReact.jsコンポーネントを作成する理由

TDDは、コードに多くの利点をもたらします。高いテストカバレッジの利点の1つは、コードをクリーンで機能的に保ちながら、コードのリファクタリングを簡単に行えることです。

以前にReact.jsコンポーネントを作成したことがある場合は、コードが非常に速く成長する可能性があることに気づきました。 それは、状態の変化とサービス呼び出しに関連するステートメントによって引き起こされる多くの複雑な条件で満たされます。

単体テストがないすべてのコンポーネントには、保守が困難になるレガシーコードがあります。 プロダクションコードを作成した後、単体テストを追加できます。 ただし、テストする必要のあるいくつかのシナリオを見落とすリスクがあります。 最初にテストを作成することで、コンポーネント内のすべてのロジックシナリオをカバーできる可能性が高くなり、リファクタリングと保守が容易になります。

React.jsコンポーネントをユニットテストするにはどうすればよいですか?

React.jsコンポーネントをテストするために使用できる戦略はたくさんあります。

  • 特定のイベントがディスパッチされたときに、 propsの特定の関数が呼び出されたことを確認できます。
  • また、現在のコンポーネントの状態を指定してrender関数の結果を取得し、それを事前定義されたレイアウトに一致させることもできます。
  • コンポーネントの子の数が予想される数と一致するかどうかを確認することもできます。

これらの戦略を使用するために、React.jsのテストで作業するのに便利な2つのツールであるJestとEnzymeを使用します。

Jestを使用した単体テストの作成

Jestは、Facebookによって作成されたオープンソースのテストフレームワークであり、React.jsとの優れた統合を備えています。 JasmineやMochaが提供するものと同様のテスト実行用のコマンドラインツールが含まれています。 また、構成がほとんどないモック関数を作成でき、アサーションを読みやすくする非常に優れたマッチャーのセットを提供します。

さらに、コンポーネントのレンダリング結果を確認および検証するのに役立つ「スナップショットテスト」と呼ばれる非常に優れた機能を提供します。 スナップショットテストを使用してコンポーネントのツリーをキャプチャし、それをファイルに保存して、レンダリングツリー(または最初の引数としてexpect関数に渡すもの)と比較します。

酵素を使用してReact.jsコンポーネントをマウントする

Enzymeは、React.jsコンポーネントツリーをマウントおよびトラバースするメカニズムを提供します。 これにより、アサーションを実行するために、独自のプロパティと状態、およびその子の小道具にアクセスできるようになります。

Enzymeは、コンポーネントの取り付けに2つの基本機能を提供します。 shallow機能とmountです。 shallow関数はルートコンポーネントのみをメモリにロードしますが、 mountは完全なDOMツリーをロードします。

EnzymeとJestを組み合わせてReact.jsコンポーネントをマウントし、その上でアサーションを実行します。

反応コンポーネントを作成するためのTDDステップ

環境の設定

この例を実行するための基本構成を備えたこのリポジトリを確認できます。

次のバージョンを使用しています。

 { "react": "16.0.0", "enzyme": "^2.9.1", "jest": "^21.2.1", "jest-cli": "^21.2.1", "babel-jest": "^21.2.0" }

TDDを使用したReact.jsコンポーネントの作成

最初のステップは、酵素の浅い関数を使用してReact.jsコンポーネントをレンダリングしようとする失敗したテストを作成することです。

 // MyComponent.test.js import React from 'react'; import { shallow } from 'enzyme'; import MyComponent from './MyComponent'; describe("MyComponent", () => { it("should render my component", () => { const wrapper = shallow(<MyComponent />); }); });

テストを実行した後、次のエラーが発生します。

 ReferenceError: MyComponent is not defined.

次に、テストに合格するための基本的な構文を提供するコンポーネントを作成します。

 // MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div />; } }

次のステップでは、JestのtoMatchSnapshot関数を使用して、コンポーネントが事前定義されたUIレイアウトをレンダリングすることを確認します。

このメソッドを呼び出した後、Jestは[testFileName].snapというスナップショットファイルを自動的に作成します。このファイルには、 __snapshots__フォルダーが追加されます。

このファイルは、コンポーネントのレンダリングに期待するUIレイアウトを表しています。

ただし、純粋なTDDを実行しようとしている場合は、最初にこのファイルを作成してから、 toMatchSnapshot関数を呼び出してテストを失敗させる必要があります。

Jestがこのレイアウトを表すためにどの形式を使用しているかわからないことを考えると、これは少し紛らわしいように聞こえるかもしれません。

最初にtoMatchSnapshot関数を実行して、スナップショットファイルで結果を確認したくなる場合があります。これは、有効なオプションです。 ただし、純粋なTDDを本当に使用したい場合は、スナップショットファイルがどのように構造化されているかを学ぶ必要があります。

スナップショットファイルには、テストの名前と一致するレイアウトが含まれています。 これは、テストの形式が次の場合を意味します。

 desc("ComponentA" () => { it("should do something", () => { … } });

これは、エクスポートセクションで指定する必要があります。 Component A should do something 1

スナップショットテストの詳細については、こちらをご覧ください。

そのため、最初にMyComponent.test.js.snapファイルを作成します。

 //__snapshots__/MyComponent.test.js.snap exports[`MyComponent should render initial layout 1`] = ` Array [ <div> <input type="text" /> </div>, ] `;

次に、スナップショットがコンポーネントの子要素と一致することを確認する単体テストを作成します。

 // MyComponent.test.js ... it("should render initial layout", () => { // when const component = shallow(<MyComponent />); // then expect(component.getElements()).toMatchSnapshot(); }); ...

renderメソッドの結果としてcomponents.getElementsを考慮することができます。

スナップショットファイルに対して検証を実行するために、これらの要素をexpectメソッドに渡します。

テストを実行した後、次のエラーが発生します。

 Received value does not match stored snapshot 1. Expected: - Array [ <div> <input type="text” /> </div>, ] Actual: + Array []

Jestは、 component.getElementsの結果がスナップショットと一致しないことを通知しています。 したがって、 MyComponentにinput要素を追加することで、このテストに合格します。

 // MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div><input type="text" /></div>; } }

次のステップは、値が変更されたときに関数を実行することにより、 inputに機能を追加することです。 これを行うには、 onChangeで関数を指定します。

最初にスナップショットを変更して、テストを失敗させる必要があります。

 //__snapshots__/MyComponent.test.js.snap exports[`MyComponent should render initial layout 1`] = ` Array [ <div> <input onChange={[Function]} type="text" /> </div>, ] `;

スナップショットを最初に変更することの欠点は、小道具(または属性)の順序が重要であることです。

Jestは、スナップショットと照合する前に、 expect関数で受け取った小道具をアルファベット順に並べ替えます。 したがって、これらをこの順序で指定する必要があります。

テストを実行した後、次のエラーが発生します。

 Received value does not match stored snapshot 1. Expected: - Array [ <div> onChange={[Function]} <input type="text”/> </div>, ] Actual: + Array [ <div> <input type=”text” /> </div>, ]

このテストに合格するには、 onChangeに空の関数を指定するだけです。

 // MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div><input onChange={() => {}} type="text" /></div>; } }

次に、 onChangeイベントがディスパッチされた後、コンポーネントの状態が変化することを確認します。

これを行うには、UIで実際のイベントを模倣するために、イベントを渡すことによって入力でonChange関数を呼び出す新しい単体テストを作成します。

次に、コンポーネントの状態inputという名前のキーが含まれていることを確認します。

 // MyComponent.test.js ... it("should create an entry in component state", () => { // given const component = shallow(<MyComponent />); const form = component.find('input'); // when form.props().onChange({target: { name: 'myName', value: 'myValue' }}); // then expect(component.state('input')).toBeDefined(); });

次のエラーが発生します。

 Expected value to be defined, instead received undefined

これは、コンポーネントにinputという状態のプロパティがないことを示しています。

このエントリをコンポーネントの状態に設定することにより、テストに合格します。

 // MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div><input onChange={(event) => {this.setState({input: ''})}} type="text" /></div>; } }

次に、新しい状態エントリに値が設定されていることを確認する必要があります。 この値はイベントから取得します。

それでは、状態にこの値が含まれていることを確認するテストを作成しましょう。

 // MyComponent.test.js ... it("should create an entry in component state with the event value", () => { // given const component = shallow(<MyComponent />); const form = component.find('input'); // when form.props().onChange({target: { name: 'myName', value: 'myValue' }}); // then expect(component.state('input')).toEqual('myValue'); }); ~~~ Not surprisingly, we get the following error. ~~ Expected value to equal: "myValue" Received: ""

最後に、イベントから値を取得し、それを入力値として設定することで、このテストに合格します。

 // MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div><input onChange={(event) => { this.setState({input: event.target.value})}} type="text" /></div>; } }

すべてのテストに合格したことを確認したら、コードをリファクタリングできます。

onChangeで渡された関数をupdateStateという新しい関数に抽出できます。

 // MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { updateState(event) { this.setState({ input: event.target.value }); } render() { return <div><input onChange={this.updateState.bind(this)} type="text" /></div>; } }

これで、TDDを使用して作成された単純なReact.jsコンポーネントができました。

概要

この例では、すべてのステップに従って、テストに失敗して合格する可能性のある最小限のコードを記述して、純粋なTDDを使用しようとしました。

一部の手順は不要に見える場合があり、スキップしたくなる場合があります。 ただし、ステップをスキップするたびに、純粋性の低いバージョンのTDDを使用することになります。

それほど厳密ではないTDDプロセスを使用することも有効であり、問​​題なく機能する可能性があります。

私がお勧めするのは、ステップをスキップすることを避け、難しいと感じても気分が悪くならないようにすることです。 TDDは習得するのが簡単ではないテクニックですが、それは間違いなく行う価値があります。

TDDおよび関連するビヘイビア駆動開発(BDD)について詳しく知りたい場合は、仲間のToptalerRyanWilcoxによるYourBossWo n'tAppreciateTDDをお読みください。