Reactコンポーネントの単体テスト

clement127(CC BY-NC-ND 2.0)によるReactコンポーネントのテストの最初の試みの写真

単体テストは、実稼働のバグ密度を40〜80%削減できる優れた分野です。単体テストには、他にもいくつかの重要な利点があります。

  • アプリケーションのアーキテクチャと保守性を改善します。
  • 実装の詳細を説明する前に開発者を開発者エクスペリエンス(API)に集中させることにより、APIと構成可能性の向上につながります。
  • ファイル保存に関する迅速なフィードバックを提供して、変更が機能したかどうかを示します。これにより、console.log()を置き換え、UIをクリックして変更をテストできます。ユニットテストの初心者は、さまざまなコンポーネントをテストする方法を理解するために、TDDプロセスに15〜30%余分に費やすかもしれませんが、TDDの経験者はTDDを使用して実装時間を節約できます。
  • 機能を追加したり、既存の機能をリファクタリングするときに自信を高めることができる優れたセーフティネットを提供します。

しかし、いくつかのことは他のものより単体テストが簡単です。具体的には、単体テストは純粋な関数に最適です。同じ入力を与え、常に同じ出力を返し、副作用のない関数です。

多くの場合、UIコンポーネントはユニットテストが簡単なカテゴリに分類されないため、TDDの規律:テストを最初に記述することが難しくなります。

最初にテストを書くことは、リストした利点のいくつかを実現するために必要です:アーキテクチャの改善、開発者のエクスペリエンス設計の改善、アプリ開発中のフィードバックの迅速化。 TDDを使用するには、訓練と訓練が必要です。多くの開発者は、テストを書く前にいじくり回すことを好みますが、最初にテストを書かないと、単体テストの多くの優れた機能を奪ってしまいます。

しかし、練習と訓練の価値があります。ユニットテストを使用したTDDを使用すると、はるかにシンプルで保守しやすく、他のコンポーネントと組み合わせて再利用しやすいUIコンポーネントを作成することができます。

私のテスト分野における最近の革新の1つは、RITEway単体テストフレームワークの開発です。これは、単純で保守しやすいテストを作成するのに役立つTapeの小さなラッパーです。

使用するフレームワークに関係なく、次のヒントは、より良く、よりテスト可能で、より読みやすく、より構成可能なUIコンポーネントを作成するのに役立ちます。

  • UIコードの純粋なコンポーネントを優先する:同じ小道具が与えられ、常に同じコンポーネントをレンダリングします。アプリの状態が必要な場合は、状態と副作用を管理するコンテナコンポーネントでこれらの純粋なコンポーネントをラップできます。
  • 純粋なリデューサー機能でアプリケーションロジック/ビジネスルールを分離します。
  • コンテナコンポーネントを使用して副作用を分離します。

純粋なコンポーネントを好む

純粋なコンポーネントは、同じ小道具が与えられ、常に同じUIをレンダリングし、副作用がないコンポーネントです。例えば。、

ReactからReactをインポートします。
const Hello =({userName})=>(
  
こんにちは、{userName}! );
デフォルトのHelloをエクスポートします。

これらの種類のコンポーネントは、一般にテストが非常に簡単です。コンポーネントを選択する方法が必要になり(この場合、あいさつclassNameで選択します)、予想される出力を知る必要があります。純粋なコンポーネントテストを作成するには、RITEwayのrender-componentを使用します。

開始するには、RITEwayをインストールします。

npm install --save-dev riteway

内部的には、RITEwayはreact-dom / server renderToStaticMarkup()を使用し、簡単に選択できるように出力をCheerioオブジェクトにラップします。 RITEwayを使用していない場合、Cheerioでクエリできる静的マークアップにReactコンポーネントをレンダリングする独自の関数を作成するために、すべてを手動で実行できます。

マークアップからCheerioオブジェクトを生成するレンダリング関数を作成したら、次のようなコンポーネントテストを作成できます。

import {describe} from 'riteway';
「riteway / render-component」からレンダーをインポートします。
ReactからReactをインポートします。
「../hello」からHelloをインポートします。
describe( 'Hello component'、async assert => {
  const userName = 'Spiderman';
  const $ = render();
  アサート({
    指定:「ユーザー名」、
    する必要があります:「正しいユーザー名に挨拶を表示します。」、
    実際:$( '。greeting')
      .html()
      。トリム()、
    期待:「こんにちは、$ {userName}!」
  });
});

しかし、それはあまり面白くない。ステートフルコンポーネント、または副作用のあるコンポーネントをテストする必要がある場合はどうでしょうか? ReactコンポーネントにとってTDDが非常に興味深いのは、その質問に対する答えが別の重要な質問に対する答えと同じであるためです。「Reactコンポーネントをより保守しやすく、デバッグしやすくするにはどうすればよいですか?」

答え:プレゼンテーションコンポーネントから状態と副作用を分離します。コンテナコンポーネントに状態と副作用の管理をカプセル化して、それを行うことができます。そして、小道具を通して純粋なコンポーネントに状態を渡します。

しかし、フックAPIは、フラットなコンポーネント階層を持ち、コンポーネントのネストをすべて忘れることができるようにしたのではありませんか?まあ、そうではありません。コードを3つの異なるバケットに保管し、これらのバケットを互いに分離しておくことをお勧めします。

  • ディスプレイ/ UIコンポーネント
  • プログラムロジック/ビジネスルール-ユーザーのために解決しようとしている問題に対処するもの。
  • 副作用(I / O、ネットワーク、ディスクなど)

私の経験では、ディスプレイ/ UIの懸念をプログラムのロジックや副作用とは別にしておけば、人生がずっと楽になります。この経験則は、React with hookを含め、これまでに使用したすべての言語とフレームワークで常に当てはまります。

クリックカウンタを作成して、ステートフルコンポーネントを示しましょう。最初に、UIコンポーネントを作成します。ボタンがクリックされた回数を示す「Clicks:13」のようなものが表示されるはずです。ボタンには「クリック」と表示されます。

表示コンポーネントの単体テストは非常に簡単です。ボタンがレンダリングされることをテストするだけで十分です(ラベルが何を言っているかは気にしません。ユーザーのロケール設定に応じて、異なる言語で異なることを言う場合があります)。正しいクリック数が表示されるようにする必要があります。 2つのテストを作成しましょう。1つはボタン表示用で、もう1つはクリック数が正しくレンダリングされるようにします。

TDDを使用する場合、2つの異なるアサーションを頻繁に使用して、適切な値がプロップから取得されるようにコンポーネントを記述したことを確認します。関数の値をハードコーディングできるようにテストを書くことができます。それを防ぐために、それぞれ異なる値をテストする2つのテストを作成できます。

この場合、というコンポーネントを作成します。このコンポーネントには、クリック数と呼ばれるクリック数の小道具があります。使用するには、コンポーネントをレンダリングし、クリックプロップを表示するクリック数に設定するだけです。

小道具からクリックカウントを引き出すことを保証できる単体テストのペアを見てみましょう。新しいファイルclick-counter / click-counter-component.test.jsを作成しましょう。

import {describe} from 'riteway';
「riteway / render-component」からレンダーをインポートします。
ReactからReactをインポートします。
'../click-counter/click-counter-component'からClickCounterをインポートします。
describe( 'ClickCounter component'、async assert => {
  const createCounter = clickCount =>
    render()
  ;
  {
    const count = 3;
    const $ = createCounter(count);
    アサート({
      指定:「クリック数」、
      する必要があります:「正しいクリック数をレンダリングします。」、
      実際:parseInt($( '。clicks-count')。html()。trim()、10)、
      予想:カウント
    });
  }
  {
    const count = 5;
    const $ = createCounter(count);
    アサート({
      指定:「クリック数」、
      する必要があります:「正しいクリック数をレンダリングします。」、
      実際:parseInt($( '。clicks-count')。html()。trim()、10)、
      予想:カウント
    });
  }
});

テストを記述しやすくするために、小さなファクトリ関数を作成するのが好きです。この場合、createCounterは注入するクリック数を取得し、そのクリック数を使用してレンダリングされたコンポーネントを返します。

const createCounter = clickCount =>
  render()
;

テストを作成したら、ClickCounter表示コンポーネントを作成します。私のテストファイルと同じフォルダーに、click-counter-component.jsという名前で同じ場所に配置しました。まず、コンポーネントフラグメントを作成して、テストが失敗するのを見てみましょう。

Reactから{React}から{フラグメント}をインポートします。
エクスポートのデフォルト()=>
  <フラグメント>
  
;

テストを保存して実行すると、現在NodeのUnhandledPromiseRejectionWarningをトリガーするTypeErrorを取得します。最終的に、NodeはDeprecationWarningの余分な段落で刺激的な警告で停止し、代わりにUnhandledPromiseRejectionErrorをスローします。選択がnullを返し、その上で.trim()を実行しようとしているため、TypeErrorが発生します。予想されるセレクターをレンダリングすることでそれを修正しましょう

Reactから{React}から{フラグメント}をインポートします。
エクスポートのデフォルト()=>
  <フラグメント>
     3 
  
;

すばらしいです。これで、1つのテストに合格し、1つのテストに失敗するはずです。

#ClickCounterコンポーネント
ok 2クリック数を指定すると、正しいクリック数が表示されます。
不可3クリックカウントを指定すると、正しいクリック数が表示されます。
  ---
    演算子:deepEqual
    予想:5
    実際:3
    at:assert(/home/eric/dev/react-pure-component-starter/node_modules/riteway/source/riteway.js:15:10)
...

修正するには、カウントを小道具として使用し、JSXでライブ小道具の値を使用します。

Reactから{React}から{フラグメント}をインポートします。
デフォルトのエクスポート({clicks})=>
  <フラグメント>
     {クリック数} 
  
;

これで、テストスイート全体が合格しました。

TAPバージョン13
#Helloコンポーネント
ok 1ユーザー名を指定:正しいユーザー名に挨拶を表示する必要があります。
#ClickCounterコンポーネント
ok 2クリック数を指定すると、正しいクリック数が表示されます。
ok 3クリック数を指定すると、正しいクリック数が表示されます。
1..3
#テスト3
#3を渡す
# OK

ボタンをテストする時間。最初に、テストを追加して失敗するのを確認します(TDDスタイル):

{
  const $ = createCounter(0);
  アサート({
    指定: 'expected props'、
    する必要があります: 'クリックボタンをレンダリングします。'、
    実際:$( '。click-button')。length、
    予想:1
  });
}

これにより、失敗したテストが生成されます。

4予想される小道具が与えられた場合:クリックボタンをレンダリングする必要があります
  ---
    演算子:deepEqual
    予想:1
    実際:0
...

次に、クリックボタンを実装します。

デフォルトのエクスポート({clicks})=>
  <フラグメント>
     {クリック数} 
    

そして、テストに合格します。

TAPバージョン13
#Helloコンポーネント
ok 1ユーザー名を指定:正しいユーザー名に挨拶を表示する必要があります。
#ClickCounterコンポーネント
ok 2クリック数を指定すると、正しいクリック数が表示されます。
ok 3クリック数を指定すると、正しいクリック数が表示されます。
ok 4予想される小道具:クリックボタンをレンダリングする必要があります。
1..4
#テスト4
#パス4
# OK

ここで、状態ロジックを実装し、イベントハンドラーを接続するだけです。

ステートフルコンポーネントのユニットテスト

ここで紹介するアプローチは、おそらくクリックカウンターではやり過ぎですが、ほとんどのアプリはクリックカウンターよりもはるかに複雑です。多くの場合、状態はデータベースに保存されるか、コンポーネント間で共有されます。 Reactコミュニティで人気のあるリフレインは、ローカルコンポーネントの状態から始めて、必要に応じて親コンポーネントまたはグローバルアプリの状態に持ち上げることです。

ローカルコンポーネントの状態管理を純粋な機能で開始すると、そのプロセスは後で管理しやすくなることがわかりました。この理由およびその他の理由(Reactライフサイクルの混乱、状態の一貫性、一般的なバグの回避など)のために、純粋なリデューサー関数を使用して状態管理を実装するのが好きです。ローカルコンポーネントの状態の場合、それらをインポートしてuseReducer Reactフックを適用できます。

Reduxのような状態管理者が管理するために状態を解除する必要がある場合は、開始する前にすでに半分になっています:単体テストとすべて。

最初に、状態レデューサー用の新しいテストファイルを作成します。これを同じフォルダーに配置しますが、別のファイルを使用します。私はこれをワンクリックカウンター/click-counter-reducer.test.jsと呼んでいます:

import {describe} from 'riteway';
インポート{レデューサー、クリック}から '../click-counter/click-counter-reducer';
describe( 'click counter reducer'、async assert => {
  アサート({
    指定:「引数なし」、
    する必要があります:「有効な初期状態を返す」、
    実際:reducer()、
    予想:0
  });
});

リデューサーが有効な初期状態を生成することを保証するために、常にアサーションから始めます。後でReduxを使用することにした場合、ストアの初期状態を生成するために、状態なしで各レデューサーを呼び出します。また、これにより、単体テストの目的で必要なときに有効な初期状態を作成したり、コンポーネントの状態を初期化したりすることが非常に簡単になります。

もちろん、対応するレデューサーファイルを作成する必要があります。私はそれをclick-counter / click-counter-reducer.jsと呼んでいます:

constクリック=()=> {};
const reducer =()=> {};
export {減速機、クリック};

空のレデューサーとアクションクリエーターをエクスポートすることから始めます。アクションの作成者や選択者などの重要な役割の詳細については、「Reduxアーキテクチャを改善するための10のヒント」を参照してください。現時点では、React / Reduxのアーキテクチャパターンについて詳しく説明しませんが、Reduxライブラリを使用しない場合でも、トピックの理解はここで何をしているのかを理解するのに大いに役立ちます。 。

最初に、テストが失敗するのを確認します。

#カウンターレデューサーをクリック
not ok 5引数なしの場合:有効な初期状態を返す必要があります
  ---
    演算子:deepEqual
    予想:0
    実際:未定義

それではテストに合格しましょう。

const reducer =()=> 0;

初期値テストに合格しますが、今度はより意味のあるテストを追加します。

  アサート({
    指定:「初期状態とクリックアクション」、
    する必要があります:「カウントにクリックを追加」、
    実際:reducer(undefined、click())、
    予想:1
  });
  アサート({
    指定:「クリック数とクリックアクション」、
    する必要があります:「カウントにクリックを追加」、
    実際:reducer(3、click())、
    予想:4
  });

テストが失敗するのを見てください(それぞれ1と4を返す必要がある場合、両方とも0を返します)。次に、修正を実装します。

減速機のパブリックAPIとしてclick()アクションクリエーターを使用していることに注意してください。私の意見では、レデューサーは、アプリケーションが直接対話しないものと考える必要があります。代わりに、アクションクリエーターとセレクターをリデューサーのパブリックAPIとして使用します。

また、アクションクリエーターとセレクター用に個別の単体テストを作成しません。減速機と組み合わせて常にテストします。レデューサーのテストとは、アクションの作成者と選択者をテストすることです。この経験則に従うと、必要なテストは少なくなりますが、個別にテストした場合と同じテストとケースカバレッジを実現できます。

constクリック=()=>({
  タイプ: 'クリックカウンタ/クリック'、
});
const reducer =(state = 0、{type} = {})=> {
  スイッチ(タイプ){
    case click()。type:return state + 1;
    デフォルト:状態を返します。
  }
};
export {減速機、クリック};

これで、すべての単体テストに合格します。

TAPバージョン13
#Helloコンポーネント
ok 1ユーザー名を指定:正しいユーザー名に挨拶を表示する必要があります。
#ClickCounterコンポーネント
ok 2クリック数を指定すると、正しいクリック数が表示されます。
ok 3クリック数を指定すると、正しいクリック数が表示されます。
ok 4予想される小道具:クリックボタンをレンダリングする必要があります。
#カウンターレデューサーをクリック
ok 5引数なしの場合:有効な初期状態を返す必要があります
ok 6与えられた初期状態とクリックアクション:カウントにクリックを追加する必要があります
ok 7クリックカウントとクリックアクションが与えられた場合:カウントにクリックを追加する必要があります
1..7
#テスト7
#7を渡す
# OK

もう1つのステップ:動作をコンポーネントに接続します。コンテナコンポーネントを使用してこれを実行できます。そのindex.jsを呼び出して、他のファイルと同じ場所に配置します。次のようになります。

import React、{useReducer} from 'react';
'./click-counter-component'からカウンターをインポートします。
import {reducer、click} from './click-counter-reducer';
エクスポートのデフォルト()=> {
  const [クリック、ディスパッチ] = useReducer(reducer、reducer());
  リターン<カウンター
    clicks = {clicks}
    onClick = {()=> dispatch(click())}
  />;
};

それでおしまい。このコンポーネントの唯一の仕事は、状態管理を接続し、単体テスト済みの純粋なコンポーネントに小道具として状態を渡すことです。テストするには、ブラウザにアプリをロードし、クリックボタンをクリックします。

これまで、ブラウザでコンポーネントを確認したり、スタイル設定を行ったりしていませんでした。カウント対象を明確にするために、ClickCounterコンポーネントにラベルとスペースを追加します。また、onClick関数を接続します。これで、コードは次のようになります。

Reactから{React}から{フラグメント}をインポートします。
エクスポートのデフォルト({clicks、onClick})=>
  <フラグメント>
    クリック数: {クリック数} &nbsp;
    

そして、すべての単体テストはまだ合格しています。

コンテナコンポーネントのテストはどうですか?コンテナコンポーネントの単体テストは行いません。代わりに、ブラウザ内で実行され、エンドツーエンドで実行されている実際のUIとのユーザーインタラクションをシミュレートする機能テストを使用します。アプリケーションには両方の種類のテスト(ユニットおよび機能)が必要であり、コンテナコンポーネント(ほとんどが上記のレデューサーを配線するような接続/配線コンポーネント)のユニットテストは、私の好みの機能テストでは冗長すぎます、適切な単体テストは特に簡単ではありません。多くの場合、さまざまなコンテナコンポーネントの依存関係をモックして、それらを機能させる必要があります。

それまでの間、副作用に依存しないすべての重要なユニットを単体テストしました。正しいデータがレンダリングされ、状態が正しく管理されていることをテストしています。また、ブラウザにコンポーネントをロードして、ボタンが機能し、UIが応答することを確認してください。

Reactの機能/ e2eテストの実装は、他のフレームワークのテストを実装するのと同じです。詳細については、「Behavior Driven Development(BDD)and Functional Testing」をご覧ください。

次のステップ

TDD Dayにサインアップ:テスト駆動開発のすべての側面に関する5時間の高品質ビデオコンテンツとインタラクティブレッスン。チーム全体のTDDスキルをレベルアップするための素晴らしい終日クラッシュコースになるように設計されています。現在のTDDの経験に関係なく、多くのことを学びます。

Eric Elliottは、分散システムの専門家であり、「Composing Software」および「Programming JavaScript Applications」という本の著者です。 DevAnywhere.ioの共同創設者として、彼は開発者にリモートで作業し、ワーク/ライフバランスを受け入れるために必要なスキルを教えています。彼は暗号化プロジェクトの開発チームを構築して助言し、Adobe Systems、Zumba Fitness、The Wall Street Journal、ESPN、BBC、およびUsher、Frank Ocean、Metallicaなどのトップレコーディングアーティストのソフトウェアエクスペリエンスに貢献しています。

彼は世界で最も美しい女性との遠隔生活を楽しんでいます。