Storybook InteractionsテストのgetBy findByの使い分けについて
対象
- Interactionsテストを書いていて同じ問題に当たったことがある人
- Interactionsテストを書き始めようと思っている人
最近、業務の中で初めてStorybookを使ったInteractionsテストを実装していてうまくelementへアクセスできないことがあったのでその原因と解決方法を紹介したいと思います。
構成
アプリケーション
- Next.js
- GraphQL
- Apollo
テスト
- Storybook
- msw
インタラクションテストについて
インタラクションテストとは、
コンポーネントのユースケースにそったユーザーイベントの設定や、状態変更を定義することで
コンポーネントの機能的な側面を検証できるテストです。
フォームに~を入力して、ファイルをアップロードして、ボタンを押したら,”保存が完了した”というダイアログが表示される。
といったような一連の流れを検証することができます。
公式訳
ページのような複雑なUIを構築すると、コンポーネントはUIをレンダリングするだけでなく、それ以上の責任を負うようになります。データを取得し、状態を管理するのです。インタラクションテストは、UIのこれらの機能的な側面を検証することを可能にします。
testの実装
ユーザーイベントの設定はuserEvent
で行います。(~をクリックしての部分)
以下はフォーム(Form)コンポーネントのテストを行なったコードです。
Formのレンダリング後にselectboxElement
の入力。その後にselectboxElementの入力値を元にデータをfetchし、fetch後、ブランド名というテキストが表示される仕様になっていました。
まず最初に実装したコードはこちらです。
参考
export const DisplayCompanyNameSelections: Story = {
storyName: '会社名に選択肢を入力',
play: async (ctx) => {
await Form.play?.(ctx);
const ui = within(ctx.canvasElement.parentNode as HTMLElement);
const selectboxElement = await ui.findByDisplayValue('選択してください');
userEvent.selectOptions(selectboxElement, "1"); // selectboxに1を入力
const companyNameText = ui.getByText('会社名'); // 会社名というテキストを取得
const companyNameElement = within(companyNameText.parentNode as HTMLElement); // brandTextの親Elementを取得
userEvent.type(companyNameElement.getByRole('textbox'), "A社"); // A社という文字列を入力
},
};
このテストを実行した時、以下のようなメッセージが出ていました。
TypeError: companyNameElement.getByRole is not a function
最後のuserEvent内の処理が問題のようでしたが、実際は、その前のconst companyNameText = ui.getByText('会社名');
この一文に問題がありました。
ここで使用しているgetByText()はuiがレンダリングされた時点で、要素がある場合は要素を返し、ない場合は、エラーを投げます。
私が最初に実装したテストもselectboxElement
の入力後にデータをfetchし、その後に会社名というテキストが表示される仕様になっていたため、getByTextでは、会社名というテキストが表示される前にテストが走っていたため、要素が見つからずエラーを返してしまっていました。
このような時、findByText()という関数が使えます。findByText()はgetByTextと同じく対応する要素を返すものですが、違いとしては要素が見つかったときに解決するPromiseを返す点です。
Promiseを返してくれるため await findByText()といった書き方をすることができ、対象の要素が見つかるまで待ってくれます。
最終的にテストをpassしたコードはこちらです。
以下のようにfindByTextにすることによってテキストが出現するまでテストの実行を止めることができます。
export const DisplayCompanyNameSelections: Story = {
storyName: '会社名に選択肢を入力',
play: async (ctx) => {
const ui = within(ctx.canvasElement.parentNode as HTMLElement);
const selectboxElement = await ui.findByDisplayValue('選択してください');
userEvent.selectOptions(selectboxElement, "1");
const companyNameText = await ui.findByText('会社名'); // await ui.findByText()に変更
const brandElement = within(brandText.parentNode as HTMLElement);
userEvent.type(companyNameElement.getByRole('textbox'), "A社");
},
};
参考
https://testing-library.com/docs/queries/about/
https://storybook.js.org/docs/react/writing-tests/interaction-testing/
Discussion