コンポーネント テストの実践

コンポーネント テストは、実用的なコードをテストするデモを開始するのに適しています。コンポーネント テストは、単純な単体テストよりも充実し、エンドツーエンド テストよりも複雑でなく、DOM の操作を実証します。より理論的には、React を使用することで、ウェブ デベロッパーはウェブサイトまたはウェブアプリをコンポーネントで構成されていると考えやすくなります。

そのため、複雑度に関係なく、個々のコンポーネントをテストすることは、新規または既存のアプリケーションのテストを検討する際の良い方法です。

このページでは、複雑な外部依存関係を持つ小さなコンポーネントのテストについて説明します。ボタンをクリックして数値が増えたことを確認するなど、他のコードと相互作用しないコンポーネントを簡単にテストできます。実際には、そのようなコードはほとんどありません。インタラクションのないコードのテストの価値は限られる可能性があります。

(これは完全なチュートリアルではありません。後のセクションの「自動テストの実践」では、チュートリアルとして使用可能なサンプルコードを使用して実際のサイトをテストする方法を説明します。このページでは、実用的なコンポーネント テストの例をいくつか取り上げています)。

テスト対象のコンポーネント

Vitest とその JSDOM 環境を使用して、React コンポーネントをテストします。これにより、コマンドラインで Node を使用して、ブラウザをエミュレートしながらテストを迅速に実行できます。

名前のリストと各名前の横にある [選択] ボタン。
ネットワークからのユーザーのリストを表示する小さな React コンポーネント。

UserList という名前のこの React コンポーネントは、ネットワークからユーザーのリストを取得し、その中の 1 つを選択できます。ユーザーのリストは、useEffect 内で fetch を使用して取得され、選択ハンドラは Context によって渡されます。コードは次のとおりです。

import React, { useEffect, useState, useContext } from 'react';
import { UserContext } from './UserContext.tsx';
import { UserRow } from './UserRow.tsx';

export function UserList({ count = 4 }: { count?: number }) {
  const [users, setUsers] = useState<any[]>([]);
  useEffect(() => {
    fetch('https://jsonplaceholder.typicode.com/users?_limit=' + count)
      .then((response) => response.json())
      .then((json) => setUsers(json));
  }, [count]);

  const c = useContext(UserContext);
  return (
    <div>
      <h2>Users</h2>
      <ul>
        {users.map((u) => (
          <li key={u.id}>
            <button onClick={() => c.userChosen(u.id)}>Choose</button>{' '}
            <UserRow u={u} />
          </li>
        ))}
      </ul>
    </div>
  );
}

この例では、React のベスト プラクティス(たとえば、useEffect 内で fetch を使用する)は示していませんが、コードベースには同様のケースが多数含まれている可能性があります。さらに重要なのは、こうしたケースは一見したところテストに苦労しているように見える可能性があることです。このコースの今後のセクションで、テスト可能なコードの記述について詳しく説明します。

この例では、次の項目をテストします。

  • ネットワークからのデータに応じて、正しい DOM が作成されることを確認します。
  • ユーザーをクリックするとコールバックがトリガーされることを確認します。

コンポーネントは千差万別です。このテストがおもしろい理由は何ですか?

  • グローバル fetch を使用して、実際のデータをネットワークにリクエストします。実際のデータは、テストでは不安定または遅い可能性があります。
  • この関数は別のクラス UserRow をインポートしますが、これは暗黙的にテストしたくない場合があります。
  • Context を使用します。これはテスト対象のコードの一部ではなく、通常は親コンポーネントによって提供されます。

簡単なテストを作成して開始する

このコンポーネントに関するごく基本的なことを簡単にテストできます。誤解のないように言うと この例はあまり役に立ちませんただし、UserList.test.tsx というピアファイルにボイラープレートをセットアップすると便利です(Vitest などのテストランナーはデフォルトで、.test.js などで終わるファイル(.tsx を含む)を実行します)。

import { vi, test, assert, afterAll } from 'vitest';
import { render } from '@testing-library/react';
import { UserList } from './UserList.tsx';
import React, { ContextType } from 'react';

test('render', async () => {
  const c = render(<UserList />);

  const headingNode = await c.findAllByText(/Users);
  assert.isNotNull(headingNode);
});

このテストでは、コンポーネントのレンダリング時に「Users」というテキストが含まれていることを確認します。このコンポーネントには、fetch をネットワークに送信するという副作用がありますが、機能します。fetch はテストの終了時にまだ進行中で、エンドポイントが設定されていません。少なくともタイムアウトを待たずには、テストの終了時にユーザー情報が表示されていることを確認できません。

fetch() のモック

モックとは、テスト用に実際の関数やクラスを、自分の制御下にあるものに置き換えることです。これは、最もシンプルな単体テストを除き、ほぼすべての種類のテストで一般的に行われています。これについては、アサーションとその他のプリミティブで詳しく説明します。

テスト用に fetch() をモックすると、テストがすぐに完了し、「実世界の」データや未知のデータではなく目的のデータが返されるようにできます。fetchグローバルです。つまり、コード内で import または require する必要はありません。

vitest でグローバルをモックするには、vi.fn() から返される特殊オブジェクトを指定して vi.stubGlobal を呼び出します。これにより、後で変更可能なモックが作成されます。これらのメソッドについては、このコースの後のセクションで詳しく説明しますが、次のコードで実際に確認することができます。

test('render', async () => {
  const fetchMock = vi.fn();
  fetchMock.mockReturnValue(
    Promise.resolve({
      json: () => Promise.resolve([{ name: 'Sam', id: 'sam' }]),
    }),
  );
  vi.stubGlobal('fetch', fetchMock);

  const c = render(<UserList />);

  const headingNode = await c.queryByText(/Users);
  assert.isNotNull(headingNode);

  await waitFor(async () => {
    const samNode = await c.queryByText(/Sam);
    assert.isNotNull(samNode);
  });
});

afterAll(() => {
  vi.unstubAllGlobals();
});

このコードはモックを追加し、ネットワーク フェッチ Response の「フェイク」バージョンを記述し、それが表示されるのを待ちます。テキストが表示されない場合は、queryByText 内のクエリを新しい名前に変更することで確認できます。テストは失敗します。

この例では、Vitest の組み込みモック ヘルパーを使用していますが、他のテスト フレームワークでも同様の方法でモックできます。Vitest は、すべてのテストの後に vi.unstubAllGlobals() を呼び出すか、同等のグローバル オプションを設定する必要があります。処理を「元に戻す」と、fetch モックが他のテストに影響を与える可能性があり、すべてのリクエストに対して、奇妙な JSON の山が返されます。

模擬インポート

UserList コンポーネント自体が UserRow というコンポーネントをインポートしていることに気付いたかもしれません。そのコードは含めていませんが、ユーザー名をレンダリングしていることがわかります。以前のテストでは「Sam」を確認しますが、これは UserList 内に直接レンダリングされないため、UserRow から取得する必要があります。

コンポーネント内でユーザーの名前がどのように移動するかを示すフローチャート。
UserListTest には UserRow の公開設定がありません。

ただし、UserRow はそれ自体が複雑なコンポーネントである可能性があります。追加のユーザーデータを取得したり、テストに関係のない副作用を起こしたりする可能性があります。そのばらつきを取り除くことで、テストの有用性が向上します。特に、テストするコンポーネントが複雑になり、その依存関係が複雑になるためです。

幸い、テストで直接使用しない場合でも、Vitest を使用して特定のインポートをモックアップできます。そのため、それらのインポートを使用するコードには、シンプルなバージョンまたは既知のバージョンが提供されます。

vi.mock('./UserRow.tsx', () => {
  return {
    UserRow(arg) {
      return <>{arg.u.name}</>;
    },
  }
});

test('render', async () => {
  // ...
});

fetch グローバルのモックと同様に、これは強力なツールですが、コードに多くの依存関係があると、使用できなくなる可能性があります。この場合の最適な修正は テスト可能なコードを記述することです

クリックしてコンテキストを入力

React や Lit などのライブラリには、Context というコンセプトがあります。サンプルコードには、ユーザーが選択された場合にメソッドを呼び出す UserContext が含まれています。これは多くの場合、コールバックが UserList に直接渡される「プロップ ドリル」の代わりに使用されます。

作成したテストハーネスでは UserContext が提供されていません。これを指定せずにクリック アクションを React テストに追加すると、最悪の場合、テストがクラッシュします。または、デフォルト インスタンスが他の場所に提供されていた場合、いくつかの動作が制御不能になります(上記の不明な UserRow と同様)。

  const c = render(<UserList />);
  const chooseButton = await c.getByText(/Choose);
  chooseButton.click();

代わりに、コンポーネントをレンダリングするときに、独自の Context を指定できます。この例では、Vitest モック関数である vi.fn() のインスタンスを使用します。これは、呼び出しが行われたことと、呼び出しが行われた引数を確認するために使用できます。この例では、これは前の例のモックされた fetch とやり取りし、テストでは、渡された ID が「sam」であることを確認できます。

  const userChosenFn = vi.fn();
  const ucForTest: ContextType<typeof UserContext> = { userChosen: userChosenFn as any };
  const c = render(
    <UserContext.Provider value={ucForTest}>
      <UserList />
    </UserContext.Provider>,
  );

  const chooseButton = await c.getByText(/Choose);
  chooseButton.click();
  assert.deepStrictEqual(userChosenFn.mock.calls, [['sam']]);

これは、テスト対象のコア コンポーネントから無関係な依存関係を削除できる、シンプルながら強力なパターンです。

まとめ

これは、テストが困難な React コンポーネントをテストして保護するためのコンポーネント テストを作成する方法を示す簡単な簡単な例です。コンポーネントがその依存関係(fetch グローバル、インポートされたサブコンポーネント、Context)と正しくやり取りすることを確認することに重点を置いています。

理解度をチェックする

React コンポーネントのテストには、どのようなアプローチが使用されましたか?

複雑な依存関係をテスト用のシンプルな依存関係でモックする
Context を使用した依存関係インジェクション
スタブ グローバル
数値が増加したことを確認する