【React】フロントエンド自動テストのための事前知識

media thumbnail

こんにちは、株式会社Gizumoでエンジニアをしている田中です。

現在ReactNext.js)で開発している自社サービスでは積極的に自動テストを書くよう方針を定めています。自動テストにはReact Testing Libraryを用いております。今回はReact Testing Libraryでテストを書く前に知っておいて損はないなと感じたことをまとめました。

前提・対象とする読者

この記事は以下を前提として読み進めてください。

  • サンプルコードはReactTypeScriptで記述
  • 実装の詳細については解説しない
  • 環境構築や必要なライブラリすべてについては触れない

また、対象とする読者は以下のような方を想定しています。

  • UIフレームワーク(React, Vue等)の自動テストに興味がある方
  • React Testing Libraryでテストを書きたい方・書いている方

テストを始める前に

自動テストの紹介へ入る前に以下の観点で前提を整理しておきます。

  • なぜテストを行うのか
  • どのようなテストを行うべきか
  • テストに使うツール

なぜテストを行うのか

ソフトウェアがビジネス要件を満たすことを保証するためにテストを行います。カバレッジを上げるため、Pull Requestを通すため等が目的ではないので注意してください。

どのようなテストを行うべきか

The more your tests resemble the way your software is used, the more confidence they can give you.
テストがソフトウェアの使用方法に似ていれば似ているほど、より信頼性が高まります。
Kent C. Dodds

React等のUIフレームワークを使ったWebアプリケーション開発では「ユーザーのユースケースを担保するための結合テストを優先的に行ない、要件に応じて他のテストを組み合わせていく」というのが良いかなと思います。
こちらは有名なReactの開発者であるKent C. Doddsが提唱したTestingTrophyのトレードオフの考え方に基き判断しています。
TestingTrophyのトレードオフについては参考記事がとてもわかりやすく書いてあります。
元記事は英語ですが短く読みやすいです。よければご覧ください。

テストに使うツールの紹介

JestReact Testing Libraryを使用します。今回はReact Testing Libraryをメインで紹介しJestについて詳細は割愛します。Jestについて詳しく知りたい方は下記リンクからご覧ください。

Jestでテストを書こう

React Testing Library

React Testing Library(略称:RTL)とはReactコンポーネントのUIテストに使用されるライブラリです。たとえば以下のようにして要素を取得しテストを行う事ができます。

test('新規作成ボタンが画面に表示されていること', () => {
  // コンポーネントをレンダリング
  render(<UserForm />);

  // テストしたい要素を取得
  const createButton = screen.getByRole('button', { name: /新規作成/i });

  // 取得した要素が画面に表示されているかを確認
  expect(createButton).toBeInTheDocument();
});

React Testing Libraryで用いるクエリの優先度

RTLではGuiding Principlesの考え方に基づき以下の3つの順番でクエリの優先度を定めています。

1. Queries Accessible to Everyone
2. Semantic Queries
3. Test IDs

参考:Priority

1. Queries Accessible to Everyone

RTLはアクセシブルな要素を取得するメソッドを優先的に使用するよう定めています。

アクセシブルとはスクリーンリーダーに対応していることやキーボード操作に対応していること、HTMLタグがそれぞれ意味を持ちSEOに対応することでサイトが発見しやすくなっていること等を意味します。

Queries Accessible to Everyoneに定義してあるものは、スクリーンリーダー等の支援技術を使用しているユーザーの体験を考慮したクエリ群です。まずはここに定義してあるクエリを優先して検討します。

この項目の中でとくに優先すべきはgetByRoleクエリでrole属性を参照することです。

2. Semantic Queries

1. Queries Accessible to Everyoneの他にHTML5等で定められている属性を参照することでできるクエリです。1. Queries Accessible to Everyoneのクエリで対応できない際に使用しますが優先度は低いです。ユースケースとしてはalt属性を参照して取得したimg要素が正しいsrc属性を保持しているか等で使用します。

const img = screen.getByAltText('foo');
expect(img).toHaveAttribute('src', 'foo.jpg');

3. Test IDs

getByTestIdgetAllByTestIddata-testid属性を参照し要素を取得できます。上記2つの方法で対応できない時のみ使用するようにします。

開発者からすると一意に参照しやすいためdata-testid属性を使用するのが一番簡単ですが、ユーザーから認知できない属性のため役割やテキストでマッチング不能な要素に対してのみ使用します。

const articleContents = screen.getAllByTestId('article-content');
expect(articleContents[0]).toHaveTextContent('title1');

テストユーティリティの定義

ありがたいことに、RTLの公式がSetupにてテスト用のユーティリティ群を定義する方法を解説してくれています。これに準拠して使いやすいテスト用のユーティリティを定義しておきましょう。

RTLの公式ではtest-utils.tsxというファイルを作成し、テスト用のユーティリティを定義しています。このファイルの目的は以下になります。

  • Custom renderの定義
  • テスト用ユーティリティモジュールの定義
  • テスト用モジュールの一元管理化

test-utilsを定義する

今回はCustom renderを定義して使用する例を紹介します。ひとつ目のCustom renderとはRTLが提供するReactコンポーネントレンダリング用のrender関数をカスタマイズしたものです。

カスタマイズの目的はテストによって様々ですが、renderProviderを含んだ状況で提供するといったことが大きい理由の1つです。
プロジェクトでRecoilTanstackQueryを使用している場合、以下のような対応を行い各テストファイルにてrender関数を呼び出すのみで済むようになります。

// GOOD🍊

import { render, RenderOptions } from '@testing-library/react';
import { ReactElement } from 'react';
import { RecoilRoot } from 'recoil';
import { QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

// INFO: 自社プロジェクト内のディレクトリ構造より参照
import { queryClient } from '@/libs/packages';

// INFO: プロバイダーラッパー
const AllTheProvider = ({ children }: { children: ReactElement }) => (
  <RecoilRoot>
    <QueryClientProvider client={queryClient}>
      {children}
      {process.env.NODE_ENV === 'development' && <ReactQueryDevtools />}
    </QueryClientProvider>
  </RecoilRoot>
);

// INFO: render関数のカスタマイズ
const customRender = (
  ui: ReactElement,
  options?: RenderOptions
  // INFO: wrapperに対してプロバイダーラッパーを渡す
) => render(ui, { wrapper: AllTheProvider, ...options });

// INFO: ライブラリのモジュールをまとめてexport
export * from '@testing-library/react';
// INFO: カスタムしたrender関数をexport
export { customRender as render };

テストファイルではテスト用のモジュールはtest-utilsからすべてimportして使用します。このようにモジュールを一元管理することで、テスト用のモジュールを変更する際にtest-utilsのみを変更すれば済むようになります。

import { render } from '@/test-utils';

import { MyExample } from './MyExample';

test('コンポーネントが表示されていること', () => {
  // INFO: 通常のrender関数と同じように使用
  expect(render(<MyExample />)).toBeInTheDocument();
});

test-utilsを使用しない場合どうするか

test-utilsを使用しない場合はrender関数を呼び出す際にwrapperオプションを使用してProviderを毎回渡す必要があります。そのためテストファイルが肥大化し、見通しが悪くなってしまいます。また、毎回wrapperオプションを渡す必要があるため、Providerを変更する際にはすべてのテストファイルを修正する必要があります

// BAD💣

import { render } from '@testing-library/react';
import { RecoilRoot } from 'recoil';
import { QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

import { queryClient } from '@/libs/packages';

import { MyExample } from './MyExample';

const wrapper = ({ children }: { children: ReactElement }) => (
  <RecoilRoot>
    <QueryClientProvider client={queryClient}>
      {children}
      {process.env.NODE_ENV === 'development' && <ReactQueryDevtools />}
    </QueryClientProvider>
  </RecoilRoot>
);

test('コンポーネントが表示されていること', () => {
  render(<MyExample />, { wrapper });
});

Suspenseを使う場合に注意する

テストを行う中でReact 18にて追加されたSuspenseを使用している場合、テスト時に以下のようなwarningが出力されることがあります。

console.error
    Warning: A suspended resource finished loading inside a test, but the event was not wrapped in act(...).

    When testing, code that resolves suspended data should be wrapped into act(...):

    act(() => {
      /* finish loading suspended data */
    });
    /* 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

warningの内容からactを使用してsuspendしているコンポーネントのローディングを待つ必要があると読み取れますがこの対応ではwarningは解消しません

suspendしているコンポーネントとはJavaScriptPromiseでいうところのPending状態のコンポーネントのことです。コンポーネントが非同期通信中で未解決状態を指します。

また、warningが出つつもテストがパスしたりしますが、できるだけwarningがないクリーンな状態でのテストパスを目指したいです。

warningの解消策

warningの解消策は、Suspensefallbackに指定してあるローディングコンテンツの終了を確認することです。以下のようにloadingというdata-testidを持つすべての要素の終了を確認します。Suspenseによるローディング表示が発生しうるコンポーネントのrenderはすべて新たに定義したcustomAsyncRenderで行うようにします。

...省略

// INFO: waitForElementToBeRemovedを使用してローディング終了を待つための関数を定義
const waitForLoadingToFinish = async () =>
  waitForElementToBeRemoved(() => [...screen.queryAllByTestId(/loading/i)], {
    timeout: 4000,
  });

// INFO: 通信が走るコンポーネント用にカスタムrender関数を定義
const customAsyncRender = async (ui: ReactElement, options?: RenderOptions) => {
  const rendered = render(ui, { wrapper: AllTheProvider, ...options });

  // INFO: ローディング終了を待つ
  await waitForLoadingToFinish();

  return rendered;
};

...省略

Arrange-Act-Assertを意識する

Arrange-Act-Assertとはテストの3つのステップを意識することです。

  • Arrange:テストの準備
  • Act:テスト対象の実行
  • Assert:テストの検証

Arrange-Act-Assertを意識することで、テストの可読性が向上し、テストの意図が明確になります

通常上記順番に記述することが多いですが、フロントエンドでテストを行う場合Arrangeの前にActを記述することが多いかもしれません。

テストの可読性を向上させるためには上記ステップごとに改行を入れることをオススメします。

// Act
const { getByText } = render(<MyExample />);

// Arrange
const textElement = getByText('Hello World');

// Assert
expect(textElement).toBeInTheDocument();

参考:PHPUnit のプラクティス

最後に

今回はReactで行うフロントエンド自動テストについて必要な前提知識をReact Testing Libraryを用いた例で紹介しました。Vue等で自動テストを導入する際にも必要な情報が含まれていると思います。自動テストを導入する際参考にしていただければ幸いです。

少しでも開発にお困りの方は
相談しやすいスペシャリストにお問い合わせください

お問い合わせ
  1. breadcrumb-logo
  2. メディア
  3. 【React】フロントエンド自動テストのた...