著者は、Vets Who Codeを、寄付のために書くプログラムの一環として選びました。
はじめに
堅牢なテストカバレッジを確保することは、Webアプリケーションを構築する上で不可欠です。 JestはJavaScriptのテストランナーであり、テストの記述と実行のためのリソースを提供します。 React Testing Libraryは、コンポーネントの実装の詳細ではなく、ユーザーのインタラクションに基づいてテストを構造化するためのテストヘルパーのセットを提供します。 JestとReact Testing Libraryは、Create React Appに事前にパッケージ化されており、アプリのテストはソフトウェアの使用方法に類似しているべきだという指針に従います。
このチュートリアルでは、さまざまなUI要素を含むサンプルプロジェクトで非同期コードとインタラクションをテストします。 Jestを使用してユニットテストを記述して実行し、コンポーネントとのやり取りを処理するためのヘルパーDOM(Document Object Model)ライブラリとしてReact Testing Libraryを実装します。
前提条件
このチュートリアルを完了するには、次のものが必要です:
-
ローカルマシンにインストールされているNode.jsバージョン14以上。macOSまたはUbuntu 18.04にNode.jsをインストールするには、macOSにNode.jsをインストールし、ローカル開発環境を作成する方法またはUbuntu 18.04にNode.jsをインストールする方法のPPAを使用してインストールするセクションの手順に従ってください。
-
ローカルマシンに
npm
バージョン5.2以上が必要です。これは、Create React Appとサンプルプロジェクトでnpx
を使用するために必要です。もしnpm
をNode.js
と一緒にインストールしていない場合は、今すぐインストールしてください。Linuxの場合、sudo apt install npm
というコマンドを使用してください。- このチュートリアルで
npm
パッケージを使用するには、build-essential
パッケージをインストールする必要があります。Linuxの場合は、sudo apt install build-essential
というコマンドを使用してください。
- このチュートリアルで
-
ローカルマシンにGitがインストールされている必要があります。Gitがコンピューターにインストールされているかどうかを確認するか、Ubuntu 20.04にGitをインストールする方法を実行してください。
-
Reactに精通しており、React.jsでコーディングする方法シリーズで開発できます。サンプルプロジェクトはCreate React Appでブートストラップされているため、別途インストールする必要はありません。
-
Jestをテストランナーやフレームワークとしてある程度理解していると役立ちますが、必須ではありません。JestはCreate React Appに事前にパッケージ化されているため、別途インストールする必要はありません。
ステップ1 — プロジェクトの設定
このステップでは、サンプルプロジェクトをクローンしてテストスイートを起動します。サンプルプロジェクトでは、3つの主要なツールを利用しています:Create React App、Jest、およびReact Testing Library。Create React Appは、単一ページのReactアプリケーションの作成をサポートするために使用されます。Jestはテストランナーとして使用され、React Testing Libraryはユーザーの操作を中心にテストを構築するためのテストヘルパーを提供します。
まず、GitHubから事前に構築されたReactアプリをクローンします。これは、特定の品種に基づいて犬の画像のコレクションの検索および表示システムを構築するためにDog APIを活用したサンプルプロジェクトであるDoggy Directoryアプリを使用します。
プロジェクトをGitHubからクローンするには、ターミナルを開いて次のコマンドを実行します:
次のような出力が表示されます:
OutputCloning into 'doggy-directory'...
remote: Enumerating objects: 64, done.
remote: Counting objects: 100% (64/64), done.
remote: Compressing objects: 100% (48/48), done.
remote: Total 64 (delta 21), reused 55 (delta 15), pack-reused 0
Unpacking objects: 100% (64/64), 228.16 KiB | 3.51 MiB/s, done.
doggy-directory
フォルダに移動します:
プロジェクトの依存関係をインストールします:
npm install
コマンドは、package.json
ファイルで定義されたすべてのプロジェクトの依存関係をインストールします。
依存関係をインストールした後、アプリのデプロイバージョンを表示するか、次のコマンドを使用してアプリをローカルで実行できます:
アプリをローカルで実行する場合、http://localhost:3000/
で開きます。ターミナルには次の出力が表示されます:
OutputCompiled successfully!
You can now view doggy-directory in the browser.
Local: http://localhost:3000
On Your Network: http://network_address:3000
起動後、アプリのランディングページは次のようになります:
プロジェクトの依存関係がインストールされ、アプリケーションが実行されています。次に、新しいターミナルを開き、次のコマンドでテストを実行してください:
npm test
コマンドは、そのテストランナーとして Jest を使用して、対話型のウォッチモードでテストを開始します。ウォッチモードでは、ファイルが変更されると自動的にテストが再実行されます。テストは、ファイルを変更するたびに実行され、その変更がテストに合格したかどうかを通知します。
最初に npm test
を実行した後、ターミナルにこの出力が表示されます:
OutputNo tests found related to files changed since last commit.
Press `a` to run all tests, or run Jest with `--watchAll`.
Watch Usage
› Press a to run all tests.
› Press f to run only failed tests.
› Press q to quit watch mode.
› Press p to filter by a filename regex pattern.
› Press t to filter by a test name regex pattern.
› Press Enter to trigger a test run.
今、サンプルアプリケーションとテストスイートが実行されているので、ランディングページのテストを開始できます。
ステップ2 — ランディングページのテスト
Jestはデフォルトで、.test.js
サフィックスのファイルと、__tests__
フォルダー内の.js
サフィックスのファイルを検索します。関連するテストファイルを変更すると、それらが自動的に検出されます。テストケースが修正されると、出力が自動的に更新されます。 doggy-directory
サンプルプロジェクト向けに準備されたテストファイルには、テストパラダイムを追加する前に最小限のコードが設定されています。このステップでは、検索を実行する前にアプリケーションのランディングページが読み込まれることを確認するテストを書きます。
エディタで src/App.test.js
を開き、次のコードを確認してください:
A minimum of one test block is required in each test file. Each test block accepts two required parameters: the first argument is a string representing the name of the test case; the second argument is a function that holds the expectations of the test.
関数内には、React Testing Libraryが提供するrender
メソッドがあります。これを使用して、コンポーネントをDOMにレンダリングします。テスト対象のコンポーネントがテスト環境のDOMにレンダリングされたら、期待される機能に対してコードをアサートし始めることができます。
次のハイライトされたコードをrender
メソッドの下に追加します:
expect
関数は、特定の結果を検証したい場合に毎回使用されます。これは、コードが生成する値を表す単一の引数を受け取ります。ほとんどのexpect
関数は、特定の値について何かをアサートするmatcher関数とペアになっています。これらのアサーションのほとんどについては、DOMで見つかる一般的な要素を確認するために提供されるjest-domによって追加のマッチャーが使用されます。たとえば、最初の行の.toHaveTextContent
はexpect
関数のマッチャーであり、getByRole("heading")
はDOM要素を取得するセレクタです。
React Testing Libraryはscreen
オブジェクトを提供しており、テストDOM環境に対してアサートするために必要な関連するクエリに簡単にアクセスできます。デフォルトでは、React Testing LibraryはDOM内の要素を見つけるためのクエリを提供しています。クエリには3つの主要なカテゴリがあります:
getBy*
(最も一般的に使用される)queryBy*
(エラーをスローせずに要素の不在をテストする場合に使用される)findBy*
(非同期コードをテストする場合に使用される)
各クエリタイプは特定の目的を果たします。後でチュートリアルで定義されます。このステップでは、最も一般的なクエリタイプであるgetBy*
クエリに焦点を当てます。異なるクエリのバリエーションの詳細なリストを確認するには、Reactのクエリチートシートを参照してください。
以下は、Doggy Directoryランディングページの注釈付き画像で、最初のテスト(ランディングページのレンダリング)がカバーする各セクションを示しています:
各expect
関数は、次のものにアサートしています(上の注釈付き画像に表示されています):
- 見出しの役割を持つ要素が部分文字列としてDoggy Directoryに一致することを期待します。
- セレクト入力が品種を選択の正確な表示値を持つことを期待します。
- 検索ボタンが選択されていないため無効になっていることを期待します。
- 検索が行われていないため、プレースホルダー画像がドキュメントに存在することを期待します。
完了したら、src/App.test.js
ファイルを保存してください。テストがウォッチモードで実行されているため、変更は自動的に登録されます。変更が自動的に登録されない場合は、テストスイートを停止して再起動する必要がある場合があります。
今、ターミナルでテストを見ると、次の出力が表示されます:
Output PASS src/App.test.js
✓ renders the landing page (172 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 2.595 s, estimated 5 s
Ran all test suites related to changed files.
Watch Usage: Press w to show more.
このステップでは、Doggy Directoryのランディングページの初期レンダリングビューを検証する初期テストを書きました。次のステップでは、非同期コードのテストのためにAPI呼び出しをモックする方法を学びます。
ステップ3 — fetch
メソッドのモック
このステップでは、JavaScriptのfetch
メソッドをモックする1つのアプローチを見ていきます。これを実現する方法はさまざまありますが、この実装ではJestのspyOn
とmockImplementation
メソッドを使用します。
外部APIに依存する場合、そのAPIがダウンしたり、応答に時間がかかる可能性があります。fetch
メソッドをモックすると、一貫性のある予測可能な環境が提供され、テストに対する信頼性が向上します。外部APIを使用するテストを正しく実行するには、APIのモックメカニズムが必要です。
注意:このプロジェクトを簡略化するために、fetchメソッドをモックします。ただし、本番向けの大規模なコードベースでは、より堅牢なソリューションであるモックサービスワーカー(MSW)などを使用することが推奨されます。
src/mocks/mockFetch.js
をエディターで開いて、mockFetch
メソッドの動作を確認してください。
mockFetch
メソッドは、アプリケーション内のAPI呼び出しに対するfetch
呼び出しの構造に密接に似ているオブジェクトを返します。 mockFetch
メソッドは、Doggy Directoryアプリ内の2つの領域で非同期機能をテストするために必要です:犬の品種のリストを表示する選択ドロップダウンと、検索が実行されたときに犬の画像を取得するAPI呼び出し。
src/mocks/mockFetch.js
を閉じます。これで、テストでmockFetch
メソッドがどのように使用されるかがわかったので、テストファイルにそれをインポートできます。 mockFetch
関数は、mockImplementation
メソッドに引数として渡され、fetch APIの模擬実装として使用されます。
src/App.test.js
に、mockFetch
メソッドをインポートするためのハイライトされたコードを追加します。
このコードは、モック実装をセットアップして解除し、各テストがレベルなプレイイングフィールドから開始するようにします。
jest.spyOn(window, "fetch");
は、DOMのグローバルwindow変数にアタッチされたfetch
メソッドへの呼び出しを追跡するモック関数を作成します。
.mockImplementation(mockFetch);
は、モックメソッドを実装するために使用される関数を受け入れます。このコマンドは元のfetch
の実装を上書きするため、アプリコード内でfetch
が呼び出されるときに実行されます。
完了したら、src/App.test.js
ファイルを保存してください。
今、ターミナルでテストを確認すると、次の出力が表示されます:
Output console.error
Warning: An update to App inside a test was not wrapped in act(...).
When testing, code that causes React state updates should be wrapped into act(...):
act(() => {
/* fire events that update state */
});
/* assert on the output */
This ensures that you're testing the behavior the user would see in the browser. Learn more at https://reactjs.org/link/wrap-tests-with-act
at App (/home/sammy/doggy-directory/src/App.js:5:31)
18 | })
19 | .then((json) => {
> 20 | setBreeds(Object.keys(json.message));
| ^
21 | });
22 | }, []);
23 |
...
PASS src/App.test.js
✓ renders the landing page (429 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.178 s, estimated 2 s
Ran all test suites related to changed files.
この警告は、予期しない状態の更新が発生したことを示しています。ただし、出力にはテストがfetch
メソッドを正常にシミュレートしたことも示されています。
このステップでは、fetch
メソッドをモックし、そのメソッドをテストスイートに組み込みました。テストは合格していますが、警告を解決する必要があります。
ステップ4 — act
の警告の修正
このステップでは、ステップ3の変更後に表面化したact
の警告の修正方法を学びます。
act
の警告は、fetch
メソッドをモックアウトしており、コンポーネントがマウントされると、APIコールが行われて犬種のリストを取得します。犬種リストは、option
要素を選択入力内に入れるための状態変数に格納されます。
以下の画像は、犬種のリストをポピュレートするために成功したAPI呼び出しが行われた後の選択入力の外観を示しています。
警告が発生するのは、テストブロックがコンポーネントのレンダリングを終えた後に状態が設定されるためです。
この問題を修正するには、src/App.test.js
内のテストケースに以下の変更を追加してください:
async
キーワードは、コンポーネントがマウントされる際に発生するAPI呼び出しの結果として非同期コードが実行されることをJestに伝えます。
A new assertion with the findBy
query verifies that the document contains an option with the value of husky
. findBy
queries are used when you need to test asynchronous code that is dependent on something being in the DOM after a period of time. Because the findBy
query returns a promise that gets resolved when the requested element is found in the DOM, the await
keyword is used within the expect
method.
変更を保存したら、src/App.test.js
で行った変更を保存してください。
新しい追加事項により、テストでのact
警告が表示されなくなることが確認できます:
Output PASS src/App.test.js
✓ renders the landing page (123 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 0.942 s, estimated 2 s
Ran all test suites related to changed files.
Watch Usage: Press w to show more.
このステップでは、非同期コードを扱う際に発生するact
警告を修正する方法を学びました。次に、Doggy Directoryアプリケーションのインタラクティブな機能を検証するための2番目のテストケースを追加します。
ステップ5 — 検索機能のテスト
最後のステップでは、検索および画像表示機能を検証するための新しいテストケースを記述します。適切なテストカバレッジを達成するために、さまざまなクエリとAPIメソッドを活用します。
エディターでsrc/App.test.js
ファイルに戻ります。ファイルの先頭に、user-event
コンパニオンライブラリとwaitForElementToBeRemoved
非同期メソッドを、以下のハイライトされたコマンドでテストファイルにインポートします。
後でこのインポートをこのセクションで使用します。
最初のtest()
メソッドの後に、新しい非同期テストブロックを追加し、以下のコードブロックでApp
コンポーネントをレンダリングします。
コンポーネントがレンダリングされたので、Doggy Directoryアプリのインタラクティブな機能を検証する関数を追加できます。
src/App.test.js
のまま、2番目のtest()
メソッド内に以下のハイライトされたコードブロックを追加します。
上記のハイライト部分は、犬種の選択をシミュレートし、正しい値が表示されていることを検証します。
getByRole
クエリは選択された要素を取得し、それをselect
変数に割り当てます。
Step 4でact
警告を修正した方法と同様に、さらなるアサーションを行う前に、cattledog
オプションがドキュメントに表示されるのを待つためにfindByRole
クエリを使用します。
以前にインポートされたuserEvent
オブジェクトは一般的なユーザーの操作を模倣します。この例では、selectOptions
メソッドが前の行で待機していたcattledog
オプションを選択します。
最後の行は、上記で選択されたcattledog
値がselect
変数に含まれていることをアサートします。
次に、Javascriptのtest()
ブロックに追加する次のセクションでは、選択された品種に基づいて犬の画像を検索するリクエストを開始し、ローディング状態の存在を確認します。
以下のハイライトされた行を追加してください:
getByRole
クエリは検索ボタンを探し出し、searchBtn
変数に割り当てます。
toBeDisabled
jest-domマッチャーは、品種の選択が行われたときに検索ボタンが無効になっていないことを検証します。
userEvent
オブジェクト上のclick
メソッドは、検索ボタンをクリックする操作を模倣します。
waitForElementToBeRemoved
async ヘルパー 関数は、Loading メッセージの表示と消失を待機し、検索 API コールが実行中である間に待機します。 waitForElementToBeRemoved
コールバック内の queryByText
は、エラーをスローせずに要素の不在を確認します。
以下は、検索が進行中の場合に表示されるローディング状態を示す画像です:
次に、次のJavascriptコードを追加して、画像と結果の数を表示を検証します:
getAllByRole
クエリはすべての犬の画像を選択し、それらを dogImages
変数に割り当てます。 クエリの *AllBy*
バリアントは、指定された役割に一致する複数の要素を含む配列を返します。 *AllBy*
バリアントは、単一の要素しか返せない ByRole
バリアントとは異なります。
モックされた fetch
実装には、レスポンス内に2つの画像URLが含まれていました。 Jestの toHaveLength
マッチャーを使用して、2つの画像が表示されていることを検証できます。
getByText
クエリは、適切な結果数が右上隅に表示されるかどうかを確認します。
toHaveAccessibleName
マッチャを使用した2つのアサーションは、個々の画像に適切な代替テキストが関連付けられていることを検証します。
A completed search displaying images of the dog based on the breed selected along with the number of results found will look like this:
新しいJavaScriptコードのすべての部分を組み合わせると、App.test.js
ファイルは次のようになります:
src/App.test.js
で行った変更を保存してください。
テストを確認すると、ターミナルの最終出力は次のようになります:
Output PASS src/App.test.js
✓ renders the landing page (273 ms)
✓ should be able to search and display dog image results (123 ms)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 4.916 s
Ran all test suites related to changed files.
Watch Usage: Press w to show more.
この最終ステップでは、Doggy Directoryアプリケーションの検索、ローディング、および表示機能を検証するテストを追加しました。最終アサーションが記述されたことで、アプリが動作することがわかります。
結論
このチュートリアルでは、Jest、React Testing Library、およびjest-domマッチャーを使用してテストケースを作成しました。段階的に構築しながら、ユーザーがUIとやり取りする方法に基づいてテストを作成しました。また、getBy*
、findBy*
、およびqueryBy*
クエリの違いや非同期コードのテスト方法について学びました。
上記のトピックについて詳しく学びたい場合は、Jest、React Testing Library、およびjest-domの公式ドキュメントを参照してください。React Testing Libraryを使用する際のベストプラクティスについては、Kent C. DoddのCommon Mistakes with React Testing Libraryを読んでみてください。Reactアプリ内でスナップショットテストを使用する方法については、How To Write Snapshot Testsをチェックしてください。