Mirrativ Tech Blog

株式会社ミラティブの開発者(バックエンド,iOS,Android,Unity,機械学習,インフラ, etc.)によるブログです

StorybookとVitestではじめるフロントエンドのかんたん自動テスト

こんにちは フロントエンドエンジニアの どじねこ です。

今回は Storybook 8.3 から追加された Vitest 統合機能を活用して、フロントエンドアプリの自動テストを強化した事例をご紹介します。

Storybook とは

storybook.js.org

Storybook は OSS として開発される UI コンポーネントワークショップです。 具体的には、フロントエンドアプリケーションの UI コンポーネントを開発する際の作業環境、ドキュメンテーションツール、テスティングフレームワークとして活用できる開発環境が提供されます。

React や Vue、Angular、Svelte、Web Component など幅広い環境に対応しており、うまく活用するとフロントエンドアプリケーションの開発をより柔軟に行えるようになります。

Storybook と Vitest の統合機能とは

そんな Storybook ですが バージョン 8.3 からテスティングフレームワークである Vitest との連携によりファーストクラスの統合テスト実行機能が追加されました。

storybook.js.org

Storybook ではコンポーネントのレイアウトや利用方法を示すストーリーに対して、UI操作を自動で行い動作を検証する Storybook Play Function を実装できます。 以下は、ブラウザで実行している Storybook 上で Play Function を実行している様子です。

この Play Function は TypeScript では以下のように実装されています。

const meta: Meta<typeof Sample> = {
  title: "Sample",
  component: Sample,
  args: {
    name: "ミラビット",
    onClick: fn(), // 呼び出し回数や引数をチェックするためにモック実装を指定
  },
};

export default meta;

export const Default: StoryObj<typeof Sample> = {
  play: async ({ canvasElement, args }) => {
    // 画面操作のためのオブジェクトを取得
    const canvas = within(canvasElement);
    // 画面に「Hello, ミラビット!」描画されている
    await canvas.findByText(`Hello, ${args.name}!`);
    // submit-button を探してクリックイベントを発火
    await userEvent.click(
      canvas.getByTestId<HTMLButtonElement>("submit-button"),
    );
    // onClickに指定した関数が1度だけ呼ばれている
    await expect(args.onClick).toHaveBeenCalledTimes(1);
  },
};

今回の機能追加では ストーリーに実装された Play FunctionVitest のテストランナーを通じてブラウザを活用したフロントエンドアプリケーションの自動テストを実行できるようになりました。

ブラウザと Vitest の連携にはPlaywrightが活用されており必要に応じて Chromium や Firefox、WebKit などのブラウザを選択可能です。また必要に応じて本物のブラウザを使用せず happy-domjsdom といったブラウザを模した環境での自動テストの実行もサポートされています。

導入方法

Storybook と Vitest の統合機能を既存の Web アプリケーション開発環境に追加で導入する方法をご紹介します。 基本的な導入手法は公式ページを参照しその通りに進めると多くの場合は問題なく完了できます。

storybook.js.org

# Storybook を >=8.4 に更新する (※細かなバグ修正が含まれているため)
npx storybook@next upgrade

# @storybook/experimental-addon-test を自動的にセットアップする
npx storybook add @storybook/experimental-addon-test

...そのはずだったのですが Vitest や Vite の利用状況によるものなのか、今回導入した環境では自動セットアップだけでは完了しなかったため途中から手動によるセットアップを行いました。以下にそれぞれご紹介します。

.storybook/vitest.setup.ts の追加

Storybook と Vitest の統合機能向けの Vitest 環境を整備する設定を実装します。 ここはデフォルトでセットアップされた状態で問題ありませんでした。

import { setProjectAnnotations } from "@storybook/react";
import { beforeAll } from "vitest";
import * as projectAnnotations from "./preview";

// This is an important step to apply the right configuration when testing your stories.
// More info at: https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations
const project = setProjectAnnotations([projectAnnotations]);

beforeAll(project.beforeAll);

vitest.config.ts の調整

担当していたアプリケーションでは、すでに Vitest を利用している状態であったため自動で設定の調整が行えませんでした。 したがって vitest.config.ts での設定は基本行わず vitest.workspace.ts による Storybook 向けの設定の追加を行いました。

細かい部分で変更を行ったのが、テスト実行に名前を追加したことと、test.root をサブディレクトリから . (プロジェクトルート)に変更したことです。後述する Storybook 用の設定とモジュールの解決をうまく行うために変更しました。

import { defineConfig } from "vitest/config";

export default defineConfig({
  // ...
  test: {
    name: "unit", // Vitest によるテストが識別しやすいように name を設定
    root: ".", // root を変更
    environment: "happy-dom",
    include: ["src/**/*.test.?(m)[jt]s?(x)"], // root からの相対パスに変更
    exclude: ["src/path/to/exclude"], // root からの相対パスに変更
  },
});

vitest.workspace.ts の追加

Vitest と、Storybook + Vitest の設定を Vitest Workspace の機能を利用して設定の追加を行います。

提供されている @storybook/experimental-addon-test はまだ試験段階のパッケージのため型定義がうまく反映されていなかったため、 @ts-expect-error で簡易的に型エラーを許容しました。いずれ修正や改善が行われるかもしれません。

// @ts-expect-error TS2307 experimental API
import { storybookTest } from "@storybook/experimental-addon-test/vitest-plugin";
import { defineWorkspace } from "vitest/config";

export default defineWorkspace([
  "vite.config.ts", // メインになる設定ファイル名
  {
    plugins: [
      storybookTest({
        // Storybook との統合機能を読み込む
        tags: {
          exclude: ["skip"], // skip とタグ付けされたストーリーの自動実行を除外する
        },
      }),
    ],
    test: {
      name: "storybook",
      environment: "happy-dom", // 実行環境に happy-dom を指定する
      setupFiles: ["./.storybook/vitest.setup.ts"], // 環境セットアップ用のファイルを指定する
      include: ["src/**/*.stories.?(m)[jt]s?(x)"], // **.stories.tsx などを実行対象にする
      exclude: ["src/path/to/exclude"], // 除外設定がある場合は足しておく
    },
  },
]);

例示では Storybook 向けの test.environment に happy-dom を指定していますが、本来公式では Playwright + Chromium での実際のブラウザを活用する構成が推奨されています。

しかしながら利用している CI 環境のリソースがある程度限定されていることに加え、試験的に推奨の構成で実行したところテストプロセスが応答しなくなるなど状態が安定しない現象が発生したため、今回はより軽量で目的を達成できる happy-dom を選択しました。

基本的なセットアップは以上です。あとは npx vitest --run コマンドを実行すると Vitest のテストと Storybook のテストの両方が実行されるようになります。

npx vitest --run

 ✓ |unit| src/components/*****.test.tsx (10)
 ✓ |unit| src/lib/*****.test.ts (41)
 ✓ |storybook| src/hooks/*****.stories.tsx (1)
 ↓ |storybook| src/components/*****.stories.tsx (0) [skipped]
 ✓ |storybook| src/components/*****.stories.tsx (1)
# ...

 Test Files  *** passed | *** skipped (***)
      Tests  *** passed (***)
   Start at  **:**:**
   Duration  *.**s (...)

正常に実行されることが確認できたらセットアップは完了です。このコマンドを npm script や CI のタスクに組み込んで自動実行するようにしましょう。

導入後の効果

Storybook と Vitest の統合機能を導入する前にあった課題と、導入後にどう変化したかをご紹介します。

ストーリーを能動的に書く理由が増えた

Web アプリケーションの開発において Storybook を作業環境として活用していない場合、ストーリーの運用が UI コンポーネントをどう使うかのカタログやドキュメンテーションツールである側面が強くなり、メンテナンスが行き届きにくい側面があるといった課題がありました。

今回の Storybook 8.3 の機能追加で、Storybook 上だけでとどまっていた Play Function による自動実行がテスティングフレームワークと公式に統合されました。これによりストーリーを書きさえすれば、UI のテストができる状態となり、ストーリー自体もメンテナンスしやすい状態になったと感じています。

また Play Function がない状態のストーリーであっても、デフォルトで正常にレンダリングできているかのチェックが行えるため Storybook を実装のために書き溜めているが活用できてないといった状況であれば、それらを自動実行するだけで実装に不備やエラーが無いかを確認できる様になる価値があると感じました。

テスト実装の学習コストが下げられた

今回の Storybook 8.3 の機能追加より以前に、Vitest に Storybook の Play Function を持ち込んで Vitest 上で実行する環境を試験的に導入しました。テスト自体は自動で実行できるようになったものの、Vitest 単体で利用したときに比較して、Storybook を統合するために必要な準備があるため、テストの実装そのものに多少フロントエンドアプリケーションの開発経験が求められることが課題となっていました。

それが今回の機能では、最初の最低限のセットアップを完了させれば Storybook に書いた Play Function そのものがテストケースとして機能し、ブラウザ上で動かしながら体験と検証できるため、学習にかかるコストが下げられたと感じています。

コンポーネントの実装品質が向上した

ブラウザで起動したStorybookを活用してUIコンポーネントの開発を行っているとしばしば、コンソールに出力される細かな警告などに気付けないことがしばしばあります。

こうした出力を見逃した状態でコマンドラインで Vitest を経由して Storybook の Play Function を実行したとき、標準出力として目に付く形で警告やエラーが出力され、目に付く機会が増えたことで実装の異常や非推奨となった機能の置き換えの必要性に気づきやすくなったと感じています。

まとめ

今回は開発が進んでいるフロントエンドアプリケーションに Storybook 8.3 から導入された Vitest との統合機能を導入し、Storybook の Play Function をテストケースとして自動実行する手法をご紹介しました。

まだ公式には試験的な機構であるものの、現状で対処できないような大きな問題も特にはありませんでした。そのため Storybook を単なるコンポーネントカタログや開発用の作業環境としてではなく、より実装に寄り添ったテスト環境としても活躍してくれるのではないかと期待しています。

関連

zenn.dev

scrapbox.io

🙌 We are hiring!

株式会社ミラティブ では一緒に開発してくれるエンジニアを募集しています!

hrmos.co

mirrativ.co.jp

mirrativ.notion.site

speakerdeck.com