この記事はニジボックスQiita記事投稿リレーの4日目の記事です🌈
StorybookのPlay functionを利用することでユーザーインタラクション後のコンポーネントの状態をStorybookで表示することができます。
各Storyに定義したPlay functionをJest側も利用することができ、これによってユーザーの挙動を模したシナリオをJestで実行し、その結果をassertできます。
最近、Next.jsを使っているプロジェクトにこのテストを導入したので、その方法を記事にしてみようと思います。
環境
- Next.js v13.4.7
- React v18.2.0
- Storybook v7.0.26
前準備
Button
というサンプルコンポーネントを用意しました。(サンプルコードなのであまり細かいところはお気になさらず...)
import { useState } from "react";
import styles from "./index.module.css";
export function Button() {
const [isClicked, setIsClicked] = useState(false);
return (
<>
{isClicked && <div onClick={() => setIsClicked(false)}>Clicked!</div>}
<button
onClick={() => setIsClicked(true)}
className={styles.button}
role="button"
>
Click Me!
</button>
</>
);
}
button
要素をクリックすると、Clicked!
というテキストが表示され、そのテキストをクリックするとテキストは非表示になる、という振る舞いをします。
次にこのコンポーネントのStoriesファイルを用意します。
import type { Meta, StoryObj } from "@storybook/react";
import { Button } from ".";
const meta: Meta<typeof Button> = {
title: "Components/Button",
component: Button,
};
export default meta;
type Story = StoryObj<typeof Button>;
export const Normal: Story = {};
Play functionを実装する
次に本題の Play function
を実装していきます。
先ほど作成した index.stories.tsx
にbutton要素をクリックするというPlay functionを持ったStoryを追加します。
export const ClickButton: Story = {};
ClickButton.play = async ({ canvasElement }) => {
const canvas = within(canvasElement);
const button = canvas.getByRole("button");
await userEvent.click(button);
};
これでクリック後の Button
Componentの状態を描画できます。
JestでPlay functionを再利用する
先ほど実装した Play function を使って、Jest側でassertをします。
少し本題から外れますが、Next.jsを使っているプロジェクトでは next/jest
を使うと簡単にJestの設定ができます。
CSS in JSライブラリなどを利用していると、コンパイルのためにJestのconfigに設定が必要なのですが、Next.jsに設定してあれば、それを引き回して利用することができます。
JestでPlay functionを呼び出し、結果をassertする
話を戻して、 Play function をJestから呼び出して、その結果をassertするテストコードを書きます。
import { composeStories } from "@storybook/react";
import { act, findByText, render } from "@testing-library/react";
import * as ButtonStories from "./index.stories";
import "@testing-library/jest-dom";
const { ClickButton } = composeStories(ButtonStories);
describe("Button component", () => {
it("should display clicked text after clicked", async () => {
const { container } = render(<ClickButton />);
await act(async () => {
await ClickButton.play({ canvasElement: container });
});
const clickedText = await findByText(container, "Clicked!");
expect(clickedText).toBeVisible();
});
});
コードそのままですが、@testing-library/react
でrenderしたStoryComponentのHTMLElementをテスト対象のplay
関数の引数に渡して実行し、その結果(Clicked!
というテキストが表示されていること)を expect(clickedText).toBeVisible();
でassertしています。
とてもシンプルな例ですが、これでPlay functionをjestから呼び出すことができました。
最後に(業務で工夫した話)
これだけだと少し寂しいので、業務で少し工夫した点について書いて終わりにしたいと思います。
例えば、ボタンクリックしてから2秒後に要素を表示する、というように先ほどのComponentに仕様変更が入ったとします。
これをそのまま実行してしまうと、当たり前ですがJestでは2秒後の要素の描画を待たずにassertしてしまうので、テストは失敗します。
この例のようにシンプルであれば、2秒待つ、という処理をテスト側に仕込んでも良いかも知れませんが、もう少し複雑なComponentをテストするとなると、単純にn秒待つ、という処理ではテストがflakyになってしまうことを危惧しました。
幸いJestには useFakeTimers
というsetTimeout
やsetInterval
のタイマーをmockできる関数が有り、今回はこちらを利用しました。
describe("Button component", () => {
beforeEach(() => {
// 各テストケース実行前にタイマーをモックする
jest.useFakeTimers();
});
afterEach(() => {
// 各テストケース実行後にタイマーを通常動作に戻す
jest.useRealTimers();
});
it("should display clicked text after clicked", async () => {
const { container } = render(<ClickButton />);
await act(async () => {
await ClickButton.play({ canvasElement: container });
jest.advanceTimersByTime(2000); // ここで2秒待っている
});
const clickedText = await findByText(container, "Clicked!");
expect(clickedText).toBeVisible();
});
});
以上、Next.jsのプロジェクトでStorybookのPlay functionをJest側で再利用する方法についてまとめてみました。
この記事はニジボックス投稿リレー企画、4日目の記事です。
次の投稿は@kalbeekatzさんです!