LoginSignup
21
13

Storybookのplay関数とTesting Libraryを使ったインタラクティブなテストの書き方

Last updated at Posted at 2023-10-04

はじめに

本記事では、Storybookのplay関数を使ってユーザーの動作をStorybook上で再現し、そのstoryを使ったインタラクティブなテストの書き方を説明します。
(ここでのインタラクティブとは、ユーザーの行う動作とその動作によってUIが変化する一連の流れを表します。)

通常のJavaScriptのテストの実装は、画面で確認できない状態でユーザーの動きをコード上で再現し、テストするという点で視覚的にわかり辛いです。
一方でStorybookを利用したテストの実装は、ユーザーの動きをブラウザ上で再現し、それをそのままテストに使うことができます。ブラウザで確認できる点で目で見てわかりやすく、とても有用的だと思っています。

今回はそんなテストの実装方法を説明するためにTodoListを用意しました。
そしてTodoListを使って、Storybookでplay関数を使用した動きのあるstoryを作成→storyを使ってインタラクティブなテストを作成するところまで実装しています。作成したTodoListはGitHub上で公開していますので、コードが気になる方は見てみてください。(Storybookやテストは実際に触ってみた方が理解しやすいと思います。)

TodoListの説明

こちらが用意したTodoListの全体です。

用意したTodoListはシンプルな機能を持っています。

  • Todoのチェック。
  • Todoの追加。
  • Todoの編集。
  • Todoの削除。

今回はTodoの追加Todoの編集について紹介します。(その他の機能についてもstoryとテストは実装しているので気になる方はクローンして触ってみてください)

コンポーネントのstoryを作成する

Storybookは公式ドキュメントに沿って導入します。
TodoListアプリのTodoListコンポーネントのstoryを実装するために、TodoList.stories.tsを作成しました。
以下は公式にも記載されているstoryを作成するための変数や型定義です。(おまじないです)

TodoList.stories.ts
import type { Meta, StoryObj } from "@storybook/react";
import { TodoList } from "@/components/TodoList";

const meta = {
  title: "TodoList",
  component: TodoList,
  parameters: {
    layout: "centered",
  },
} satisfies Meta<typeof TodoList>;

export default meta;
type Story = StoryObj<typeof meta>;

続いてDefaultのstoryを作成します。storyには用意したStory型で型定義します。

TodoList.stories.ts
// const meta = {
// ~~おまじない~~
// type Story = StoryObj<typeof meta>;

export const Default: Story = {}; // Defaultのstory

これでTodoListコンポーネントをStorybookで表示できました。

それではTodoを追加する・Todoを編集する機能についてstoryを作成していきます。

Todoを追加するstory

Todoを追加するためにはユーザーは以下の動作が必要です。

  1. テキストボックスに追加するTodoの文字列を入力する。
  2. Add Todoのボタンをクリックする。

このユーザーの動作をstory上で再現するためにStorybookのplay関数を使用します。
play関数を使用するために公式ドキュメントに沿っていくつかライブラリをインストールします。
テストに利用するためのstory作成なので、@storybook/testing-libraryというTesting LibraryをStorybookで使用するためのライブラリをインストールしています。

それではTodoを追加するAddTodostoryを作成します。
以下の変数AddTodoのように、Storybookのplay関数はstoryのプロパティの一つとして設定します。valueにはasync関数で非同期処理を実装することができます。

TodoList.stories.ts
import { within, userEvent } from "@storybook/testing-library"; // ←追加

// const meta = {
// ~~おまじない~~
// type Story = StoryObj<typeof meta>;

export const Default: Story = {};

export const AddTodo: Story = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    await userEvent.type(canvas.getByRole("textbox"), "勉強する"); // テキストボックスに文字を入力
    await userEvent.click(canvas.getByRole("button", { name: "Add Todo" })); // Add Todoをクリック
  },
};

AddTodoにplay関数を実装することで、このstoryでは、自動でTodoが追加されるようになりました。

ではplay関数の中身を説明します。
ここでは動作を再現するために、@storybook/testing-librarywithinuserEventを使用しています。

play関数の引数のcanvasElementにはそのstoryに描画するHTMLElementが入ります。そこでimportしたwithincanvasElementを渡すことでそのstoryのHTMLElementを操作できるようになります。

const canvas = within(canvasElement);

続いてuseEventを使用します。ここではtype()によってユーザーの入力を、click()によってユーザーのクリックを再現しています。

await userEvent.type(canvas.getByRole("textbox"), "勉強する"); // テキストボックスに文字を入力
await userEvent.click(canvas.getByRole("button", { name: "Add Todo" })); // Add Todoをクリック

それぞれの引数にはcanvas.getByRole()が使われ、storyから対象のHTMLElementを取得しています。(getByRoleは対象のHTMLElementからroleで絞り込んで取得します。詳細は公式を参照ください。)
この一連の動作は順に行われるようにawaitを先頭に付けています。

このようにStorybookのplay関数を用いることで、ユーザーの動作を再現したstoryを作成することができました。次にもうひとつ、Todoを編集するstoryを作成します。

Todoを編集するstory

Todoを編集するためには以下の動作が必要です。

  1. 編集したいTodoのEditボタンをクリックする。
  2. 編集用のテキストボックスに文字列を入力して編集する。
  3. Doneボタンをクリックする。

この順番で動作させるためには以下のようにplay関数を実装します。

TodoList.stories.ts
import { within, userEvent, fireEvent } from "@storybook/testing-library";

// const meta = {
// ~~おまじない~~
// type Story = StoryObj<typeof meta>;

export const Default: Story = {};
export const AddTodo: Story = {...};

export const EditTodo: Story = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    const editButton = canvas.getAllByRole("button", { name: "Edit" })[0];
    await userEvent.click(editButton); // Editボタンをクリック
    const textbox = canvas.getByDisplayValue("買い物に行く");
    fireEvent.change(textbox, { target: { value: "勉強する" } }); // 修正文字列を入力
    await userEvent.click(canvas.getByRole("button", { name: "Done" })); // Doneボタンをクリック
  },
};

上記のようにplay関数を実装することで、EditTodostoryではこのように動作します。

EditTodoではAddTodoにはなかったfireEventを使用しています。

await userEvent.click(editButton); // Editボタンをクリック
const textbox = canvas.getByDisplayValue("買い物に行く");
fireEvent.change(textbox, { target: { value: "勉強する" } }); // 修正文字列を入力
await userEvent.click(canvas.getByRole("button", { name: "Done" })); // Doneボタンをクリック

fireEventuserEventの違いですが、fireEventは「DOMイベントを発火させるため」の関数で、userEventは「ユーザーが実際に行う操作を表現するため」の関数です。fireEventuserEventのどちらでも実装できるという場合がありますが、公式では「ユーザーの動作をテストする場合には、できるだけuserEventを使うことが推奨する」と記載されています。(ただ今回はテキストボックスの文字列を変更するのにfireEventの方が実装しやすかったためこのように実装しています。)

これで、テストに使用するためのstoryの作成が完了しました。
次項ではメインである、Storybookのplay関数を使用したインタラクティブなテストを実装していきます。

play関数を使ってテストを作成する

テストを書くための準備ができたので次にテストを実装していきます。
play関数を使ったテストを紹介する前にDefaultstoryをベースに、Storybookを取り入れた一般的なReactコンポーネントテストの実装方法を説明します。

Storybookを使ったテストの書き方

こちらがDefaultstoryを使って実装したテストの全体像になります。

TodoList.test.tsx
import { render, screen } from "@testing-library/react";
import { composeStories } from "@storybook/react";
import * as stories from "@/stories/TodoList.stories";
import "@testing-library/jest-dom";

const { Default } = composeStories(stories);

describe("TodoList.tsxのテスト", () => {
  test("TodoListにTodoが表示される。", () => {
    render(<Default />);
    expect(screen.getAllByRole("listitem")[0]).toBeInTheDocument();
  });
}

TodoList.stories.tsxからimportしたstoryはcomposeStoriesというテストでstoryを使うための関数に渡しています。

TodoList.test.tsx
import { composeStories } from "@storybook/react";
import * as stories from "@/stories/TodoList.stories";

const { Default } = composeStories(stories);

@testing-library/jest-domというライブラリをimportしていますが、これはコンポーネントをテストするときに、便利なカスタムマッチャーと呼ばれるメソッド(toBeInTheDocument()など)を使えるようにするためにimportしています。

import "@testing-library/jest-dom";

テストで使っているコンポーネントの中のElementをテストするには、render()screen()を使います。

TodoList.test.tsx
import { render, screen } from "@testing-library/react";

describe("TodoList.tsxのテスト", () => {
  test("TodoListにTodoが表示される。", () => {
    render(<Default />);
    expect(screen.getAllByRole("listitem")[0]).toBeInTheDocument();
  });
}

testの中身ですが、まずDefaultコンポーネントをレンダリング(render())し、次にその画面からElementにアクセスして(screen.getAllByRole())テストしています。
これでDefaultのstoryはテストできました。
続いてplay関数を使ったstoryをテストしていきます。

Todoを追加するテスト

Todoを追加する動作をテストするには以下のように実装します。
storyで既に「ユーザーの動作」は再現できているので、その動作を行なった時に期待する値が取得できるかテストするだけです。

TodoList.test.tsx
test("フォームにテキストを入れて「Add Todo」ボタンをクリックするとTodoが追加される。", async () => {
  const { container } = render(<AddTodo />);
  expect(screen.getAllByRole("listitem")).toHaveLength(3); // 初期表示のTodoの個数を確認
  await AddTodo.play({ canvasElement: container }); // play関数を実行。
  expect(screen.getAllByRole("listitem")).toHaveLength(4); // Todoが4個になっていることを確認
});

ここでの注目すべき点は、AddTodo.play()です。storyで作成したplay関数は各storyのプロパティとして参照できます。Story.play()を実行するだけでstoryに登録されたplay関数(ここではTodoを追加する動作)を実行することができます。たったこれだけでテスト内で再現したい動作を実装することができます。
初見の人でもplay関数の存在を知っていれば、
「storyで作成したplay関数を使って特定の動作を実行してるんだな。」と容易にテスト内容を理解でき、実際のstoryの方で動きを確認することもできます。
ではもう一つのTodoを編集するテストを見ていきましょう。

Todoを編集するテスト

Todoを編集するテストは以下のように実装しました。

TodoList.test.tsx
test("TodoListの「Edit」ボタンをクリックするとTodoが編集できる", async () => {
  const { container } = render(<EditTodo />);
  expect(screen.getByRole("checkbox", { name: "買い物に行く" })).toBeInTheDocument(); // 初期表示のTodoを確認
  await EditTodo.play({ canvasElement: container }); // play関数の実行
  expect(screen.getByRole("checkbox", { name: "勉強する" })).toBeInTheDocument()); // 編集後のTodoを確認
});

こちらのテストでもTodoを追加するテストと同様に、まずはコンポーネント上に「買い物に行く」という編集前のTodoがあることを確認し、次にplay関数を実行、最後にplay関数を実行後編集したTodoがコンポーネント上に存在することを確認しています。

このように、storybook側のplay関数であらかじめ動作を含めたstoryを作成することで、ちょっと複雑なユーザーの動きをテスト上で再現することなく、シンプルなテストコードにまとめることができました。

以上がStorybookのplay関数とTesting Libraryを使ったインタラクティブなテストの書き方になります。

さいごに

Storybookのplay関数を使ったテストの実装方法は、テストを見た時に使われているstoryを参照し、実際の動作を目で確認することができる、そしてこのテスト項目が何をテストしているのか容易に理解できることが最大のメリットだと思います。

個人的には、テスト側でユーザーの動作を再現するよりも、Storybook側で先にユーザーの動作を再現する方がブラウザで表示して目で見て確認しながら実装できるのでコードを書き易かったです。まだplay関数を使ったことない方はぜひ触ってみてください。

21
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
21
13