こんにちは、株式会社Gizumoでエンジニアをしている田中です。
現在React
(Next.js
)で開発している自社サービスでは積極的に自動テストを書くよう方針を定めています。自動テストにはReact Testing Library
を用いております。今回はReact Testing Library
でテストを書く前に知っておいて損はないなと感じたことをまとめました。
目次
前提・対象とする読者
この記事は以下を前提として読み進めてください。
- サンプルコードは
React
とTypeScript
で記述 - 実装の詳細については解説しない
- 環境構築や必要なライブラリすべてについては触れない
また、対象とする読者は以下のような方を想定しています。
- 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
のトレードオフについては参考記事がとてもわかりやすく書いてあります。
元記事は英語ですが短く読みやすいです。よければご覧ください。
- 【フロントエンド】コンポーネント指向 React / Vue のテスト方針(参考記事)
- Static vs Unit vs Integration vs E2E Testing for Frontend Apps(元記事)
テストに使うツールの紹介
JestとReact Testing Libraryを使用します。今回はReact Testing Library
をメインで紹介し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
getByTestId
やgetAllByTestId
でdata-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
関数をカスタマイズしたものです。
カスタマイズの目的はテストによって様々ですが、render
にProvider
を含んだ状況で提供するといったことが大きい理由の1つです。
プロジェクトでRecoil
やTanstackQuery
を使用している場合、以下のような対応を行い各テストファイルにて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
しているコンポーネントとはJavaScript
のPromise
でいうところのPending
状態のコンポーネントのことです。コンポーネントが非同期通信中で未解決状態を指します。
また、warning
が出つつもテストがパスしたりしますが、できるだけwarning
がないクリーンな状態でのテストパスを目指したいです。
warning
の解消策
warning
の解消策は、Suspense
のfallback
に指定してあるローディングコンテンツの終了を確認することです。以下のように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();
最後に
今回はReact
で行うフロントエンド自動テストについて必要な前提知識をReact Testing Library
を用いた例で紹介しました。Vue
等で自動テストを導入する際にも必要な情報が含まれていると思います。自動テストを導入する際参考にしていただければ幸いです。