多くのエンジニアが悩むUIコンポーネントのテスト、実は「属人化」と「非効率」の温床です。
手動での動作確認は時間がかかり、見落としも発生しがち。かといって、テストコードをゼロから書くのは手間がかかり、コンポーネントの改修と同時にテストも更新するのは骨の折れる作業です。
本記事では、この課題に対し、StorybookとVitestを連携させることで、UIコンポーネントのテストを効率的に自動化し、品質向上と開発体験改善を実現する具体的な方法を解説します。最新のStorybook 10.xとVitest 4.xに対応した設定から、インタラクションテストの実装、CI/CDへの統合、そしてよくあるトラブルシューティングまで、現場で役立つ実践的な内容をお届けします。
この記事を読めば、あなたのUIコンポーネント開発は、より信頼性が高く、スピーディーになるでしょう。
StorybookとVitest連携の目的とメリット
このセクションでは、なぜStorybookとVitestを連携させるのか、その具体的なメリットと解決できる課題について解説します。
UIコンポーネントのテストは、その見た目だけでなく、ユーザー操作に対する反応も検証する必要があります。Storybookはコンポーネントの様々な状態をカタログ化し、視覚的に確認できる強力なツールです。ここにVitestを組み合わせることで、ストーリーとして定義された各コンポーネントの状態に対し、自動的なインタラクションテストを実行できるようになります。
この連携により、以下のメリットが期待できます。
- コンポーネントの状態と動作の網羅的な検証: Storybookのストーリーがコンポーネントの多様な状態を表現し、そのストーリー内でVitestによるインタラクションテストを実行することで、コンポーネントの見た目と動作の両方を高い信頼性で検証できます。
- 開発体験の向上: 開発者はStorybook UIでコンポーネントを視覚的に確認しながら、同時にテストを記述・デバッグできます。Vitestの高速なフィードバックループも開発効率を高めます。
- 実際のブラウザ環境でのテスト: VitestのBrowser ModeとStorybook Vitestアドオンにより、JSDOMのようなシミュレート環境ではなく、実際のブラウザでテストを実行できるため、より信頼性の高いUIテストが実現します。
- CI/CDパイプラインへの統合: 自動化されたUIテストをCI/CDパイプラインに組み込むことで、コード変更ごとにコンポーネントの品質を保証し、デプロイ前の問題を早期に発見できます。
- テストコードの重複削減: ストーリー自体をテストケースとして活用することで、テストコードの重複を減らし、コンポーネントとテストの関連性を高めます。
最新の技術スタックと主要な変更点(2026年7月時点)
このセクションでは、本記事で利用するStorybookとVitestの最新バージョンと、その主要な変更点について概説します。
本記事は、以下の最新安定版をベースに解説します。
-
Storybook:
10.5.0(2026年7月10日公開) -
Vitest:
4.1.10(2026年7月6日公開)
Storybookの主要な変更点 (v9.0以降)
-
Vitestとの連携強化:
@storybook/addon-vitestにより、すべてのStorybookストーリーをVitestテストとして実行可能になりました。 - コンポーネントテスト機能の強化: インタラクションテスト、アクセシビリティテスト、ビジュアルテスト、カバレッジレポートのサポートが向上しています。
- バンドルサイズの軽量化: v9.0で48%軽量化され、パフォーマンスが改善されています。
- ESM-only: v10.0以降はESM(ECMAScript Modules)のみをサポートします。
-
@storybook/addon-vitest: StorybookのVitestアドオンで、ストーリーをVitestテストに自動変換し、ブラウザ環境でコンポーネントのレンダリングと動作をテストします。 -
play関数: ストーリー内でユーザーインタラクションをシミュレートし、結果をアサートするための関数です。Vitestアドオンと連携して自動テストが可能です。 -
storybook/test:expect,userEvent,withinなどのテストユーティリティを提供し、VitestとTesting LibraryをStorybookに統合します。
Vitestの主要な変更点 (v4.0以降)
- Browser Modeの安定化: JSDOMのようなシミュレート環境ではなく、実際のブラウザ環境でUIコンポーネントをテストできる機能が安定しました。
- Visual Regression Testingのサポート: コンポーネントのスクリーンショットを比較し、意図しない視覚的な変更を検出する機能が強化されました。
- Playwright Tracesのサポート: PlaywrightのTrace Viewerで詳細なトレースファイルを生成し、デバッグを容易にします。
-
test.extendパターン: 型推論をサポートする新しいビルダーパターンが導入されました。 - Vite Native Test Runner: Vite上に構築されており、Viteの設定やプラグインを再利用できるため、設定がシンプルになります。
-
Jest互換:
expect, スナップショット、カバレッジなど、Jestからの移行が容易です。
StorybookとVitest連携の実装手順とコード例
このセクションでは、StorybookとVitestを連携させ、UIコンポーネントの自動テスト環境を構築する具体的な手順とコード例を解説します。
1. StorybookとVitestアドオンのインストール
まず、必要なパッケージをインストールします。Storybookが未設定の場合は、npx storybook@latest init で初期化してください。
# Storybookが未設定の場合、まずStorybookを初期化
npx storybook@latest init
# VitestとStorybook Vitestアドオンをインストール
npm install @storybook/addon-vitest vitest --save-dev
# または yarn add -D @storybook/addon-vitest vitest
# または pnpm add -D @storybook/addon-vitest vitest
2. Storybook設定ファイル(.storybook/main.ts)でアドオンを登録
インストールしたVitestアドオンをStorybookに認識させるため、main.ts に登録します。また、Viteの依存関係最適化によるテスト失敗を回避するための設定も追加します。
// .storybook/main.ts
import type { StorybookConfig } from '@storybook/react-vite'; // 使用しているフレームワークに合わせて変更 (例: @storybook/vue3-vite)
const config: StorybookConfig = {
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
'@storybook/addon-a11y', // アクセシビリティテスト用 (任意)
'@storybook/addon-vitest', // Vitestアドオンを登録
],
framework: {
name: '@storybook/react-vite', // 使用しているフレームワークに合わせて変更
options: {},
},
docs: {
autodocs: 'tag',
},
// よくあるエラー回避策として、Viteの依存関係最適化設定を追加
// 特にCI環境や初回実行時に 'Cannot read properties of null (reading 'useState')' などのエラーを回避
viteFinal: async (config, { configType }) => {
if (configType === 'DEVELOPMENT') {
config.optimizeDeps = config.optimizeDeps || {};
config.optimizeDeps.include = [
...(config.optimizeDeps.include || []),
'react/jsx-dev-runtime', // Reactプロジェクトの場合。エラーログで指摘された依存関係を追加
// 例: 'lodash', '@emotion/react', など
];
}
return config;
},
};
export default config;
3. Vitest設定ファイル(vitest.config.ts)の設定
Vitest 4.0以降では、Storybookテスト用に個別のテストプロジェクトを使用することが推奨されます。これにより、ユニットテストとUIコンポーネントのテストを異なる設定で実行できます。
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react'; // 使用しているフレームワークのViteプラグイン (例: @vitejs/plugin-vue)
import { storybookTest } from '@storybook/addon-vitest';
export default defineConfig({
plugins: [react()], // 使用しているフレームワークのViteプラグインを適用
test: {
// デフォルトのブラウザ設定 (Storybookプロジェクトで上書きされるが、他のブラウザテストにも影響)
browser: {
enabled: true,
name: 'chromium', // または 'firefox', 'webkit'
headless: true, // CI環境ではtrueが推奨
},
// Storybookテスト用に個別のプロジェクトを設定 (Vitest >= 4.0)
projects: [
{
name: 'storybook', // プロジェクト名
test: {
include: ['**/*.stories.{js,jsx,ts,tsx}'], // Storybookのストーリーファイルをテスト対象に含める
setupFiles: ['./.storybook/vitest.setup.ts'], // Storybookのプレビュー設定を読み込む
browser: {
enabled: true,
name: 'chromium', // Storybookテスト用のブラウザ
headless: true,
},
// Storybook Testプラグインを適用
plugins: [
storybookTest({
// Storybookの起動スクリプトを指定 (watchモードでデバッグする場合)
// 'pnpm storybook --no-open' は、Storybookをバックグラウンドで起動し、ブラウザを開かない
storybookScript: 'pnpm storybook --no-open',
// StorybookのURLを指定 (CIなどで既にStorybookが起動している場合)
// storybookUrl: 'http://localhost:6006',
}),
],
// CI環境での不安定性を考慮し、タイムアウトを長めに設定することも検討
// testTimeout: 30000,
// hookTimeout: 10000,
},
},
// 他のVitestテストプロジェクト (ユニットテストなど) を追加可能
{
name: 'unit',
test: {
include: ['src/**/*.test.{js,jsx,ts,tsx}'],
environment: 'jsdom', // ユニットテストは通常JSDOMで十分
// その他のユニットテスト設定
},
},
],
},
resolve: {
alias: {
// React 18以外のバージョンを使用している場合の回避策
// '@storybook/react-dom-shim': '@storybook/react-dom-shim/dist/react-16', // React 16の場合
},
},
});
4. Storybookのプレビュー設定をVitestに適用(.storybook/vitest.setup.ts)
このファイルは、VitestテストランナーがStorybookのプレビュー設定(グローバルなデコレーターや引数など)を読み込むために必要です。これにより、Storybook UIで表示されるコンポーネントと同じ状態でテストを実行できます。
// .storybook/vitest.setup.ts
import { beforeAll } from 'vitest';
import { setProjectAnnotations } from '@storybook/react'; // 使用しているフレームワークに合わせて変更 (例: @storybook/vue)
import * as previewAnnotations from './preview'; // Storybookのプレビュー設定をインポート
// 使用しているアドオンのプレビューアノテーションをインポート (例: a11yアドオン)
import * as a11yAddonAnnotations from '@storybook/addon-a11y/preview';
// Storybookのプレビュー設定とアドオンのアノテーションをVitestに適用
// これにより、Storybookのグローバルな設定やデコレーターがテストに反映されます。
setProjectAnnotations([previewAnnotations, a11yAddonAnnotations]);
// 必要に応じて、Storybookの初期化処理などを追加
beforeAll(async () => {
// 例: Storybookの初期化処理や、テストに必要なグローバルなセットアップ
});
5. ストーリーファイルでのインタラクションテストの記述例
play 関数は、ストーリーがレンダリングされた後に実行される関数です。この中で、storybook/test から提供されるユーティリティ(expect, userEvent, within)を使用して、ユーザーインタラクションをシミュレートし、コンポーネントの動作をアサートします。
// src/components/Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { expect, userEvent, within, fn } from '@storybook/test'; // storybook/testからインポート
import { Button } from './Button';
const meta: Meta<typeof Button> = {
component: Button,
title: 'Example/Button',
argTypes: {
backgroundColor: { control: 'color' },
},
args: { onClick: fn() }, // モック関数をargsとして渡す
tags: ['autodocs', 'test'], // 'test'タグを付けることで、Vitestでテスト対象として認識されやすくなる
};
export default meta;
type Story = StoryObj<typeof Button>;
export const Primary: Story = {
args: {
primary: true,
label: 'Button',
},
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement); // ストーリーのDOM要素を取得
const button = canvas.getByRole('button', { name: /Button/i }); // ボタン要素を取得
await userEvent.click(button); // ボタンをクリックするインタラクションをシミュレート
await expect(args.onClick).toHaveBeenCalled(); // onClick関数が呼ばれたことをアサート
},
};
export const Secondary: Story = {
args: {
label: 'Secondary Button',
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const button = canvas.getByRole('button', { name: /Secondary Button/i });
await userEvent.hover(button); // ホバーイベントをシミュレート
// ホバー時のスタイル変更などをアサート (例: スナップショットテストやビジュアルテストと組み合わせる)
await expect(button).toHaveStyle('background-color: lightgray'); // 例: 特定のスタイルが適用されたことをアサート
},
};
6. package.json のスクリプト
テストを簡単に実行できるように、package.json にスクリプトを追加します。
{
"scripts": {
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build",
"test": "vitest run --project=storybook", // Storybookテストを実行
"test:watch": "vitest --project=storybook", // Storybookテストをウォッチモードで実行
"test:all": "vitest" // 全てのVitestテストプロジェクトを実行 (ユニットテスト含む)
}
}
これで、npm run test を実行すると、設定したStorybookのインタラクションテストがVitestによってブラウザ環境で自動的に実行されます。
StorybookとVitest連携でつまずきやすいポイントと解決策
このセクションでは、StorybookとVitestの連携環境を構築・運用する際によく遭遇するエラーやハマりどころと、その具体的な回避策について解説します。特にCI環境での不安定性は、多くのエンジニアが悩むポイントです。
1. Cannot read properties of null (reading 'useState') エラー / Viteの依存関係最適化によるテスト失敗
-
原因: Viteの依存関係最適化がテスト実行中に発生し、
react/jsx-dev-runtimeなどがプリ最適化されていないために、Reactフックが一時的に利用できなくなることで発生します。特にCI環境やViteキャッシュをクリアした初回実行時に発生しやすいです。 -
回避策:
.storybook/main.tsのviteFinalで、問題となる依存関係をoptimizeDeps.includeに明示的に追加します。エラーログで「new dependencies optimized」と表示されるものを特定して追加することが重要です。// .storybook/main.ts (抜粋) viteFinal: async (config, { configType }) => { if (configType === 'DEVELOPMENT') { config.optimizeDeps = config.optimizeDeps || {}; config.optimizeDeps.include = [ ...(config.optimizeDeps.include || []), 'react/jsx-dev-runtime', // 他にもエラーログで指摘された依存関係を追加 (例: 'lodash', '@emotion/react') ]; } return config; },
2. m.createRoot is not a function エラー (Reactプロジェクト)
-
原因: React 18以外のバージョンを使用しているプロジェクトで発生することがあります。Storybookの内部でReact 18のAPIが期待されている場合に、古いReactバージョンが使用されると発生します。
-
回避策:
vitest.config.tsでエイリアスを設定し、正しいReactバージョンが使用されるようにします。// vitest.config.ts (抜粋) resolve: { alias: { // React 16を使用している場合 '@storybook/react-dom-shim': '@storybook/react-dom-shim/dist/react-16', // React 17を使用している場合 // '@storybook/react-dom-shim': '@storybook/react-dom-shim/dist/react-17', }, },
3. CI環境でのテスト失敗 (Failed to fetch dynamically imported module や Cannot connect to the iframe)
- 原因: CI環境で多数のテストを同時に実行すると、リソース不足やタイミングの問題で発生することがあります。Viteの依存関係最適化がCIで再ロードを引き起こし、Vitestのランナーを中断させることも原因となります。
-
回避策:
- CIワークフローで、Storybookインスタンスが完全に起動し、到達可能であることを確認する「ウォームアップ」ステップを追加する。
-
optimizeDeps.includeに必要な依存関係のみを含め、CIのログで「new dependencies optimized」と表示されるものを特定して追加する。 - Viteの専用キャッシュディレクトリを使用し、CIワークフローで
.viteおよびStorybookのキャッシュディレクトリをキャッシュする。これにより、次回以降の実行速度が向上し、不安定性を低減できます。 - テストの並列実行数を制限する (
pool: 'threads'やブラウザインスタンス数の削減)。 - テストタイムアウトを増やす (
vitest.config.tsのtestTimeout,hookTimeout)。 - カバレッジレポートの生成をCIで無効にする(必要な場合を除く)。これはI/O負荷を減らすためです。
- テストが失敗した場合に、CIでテストコマンドを2回実行してみる(初回は依存関係の最適化やタイミングの問題で失敗する可能性があるが、2回目は成功することがある)。
4. Error: Vitest failed to find the current suite
-
原因: ストーリーの変換方法に関連していることが多いです。Viteの依存関係最適化の警告 (
new dependencies optimized: lodashなど) が原因で、テスト中に再ロードが発生し、Vitestのテストスイート管理に干渉する可能性があります。 -
回避策: Viteの設定の
optimizeDeps.include配列に依存関係を追加して、テスト中の依存関係最適化を防ぎます。これは前述のuseStateエラーの回避策と共通する部分です。
これらの回避策を適切に適用することで、StorybookとVitest連携によるUIテストの安定性を大幅に向上させることができます。
設計上のトレードオフとUIテストのベストプラクティス
このセクションでは、StorybookとVitestを使ったUIテスト戦略を立てる上で考慮すべき設計上のトレードオフと、効果的なテスト運用を実現するためのベストプラクティスについて解説します。
トレードオフ
UIテストには様々なアプローチがあり、それぞれにメリットとデメリットが存在します。プロジェクトの要件やチームの状況に合わせて、最適なバランスを見つけることが重要です。
-
Storybookストーリーとテストコードの結合:
- メリット: ストーリーがコンポーネントの様々な状態を表現するため、そのストーリーに直接テストを記述することで、テストコードの重複を減らし、コンポーネントとテストの関連性を高めることができます。開発者はStorybook UIでコンポーネントを視覚的にデバッグしながらテストできます。
-
デメリット: ストーリーファイルが肥大化する可能性があります。Storybook固有のテストユーティリティ (
storybook/testからのexpectなど) を学習する必要がある場合があります。
-
JSDOM vs. ブラウザ環境でのUIテスト:
-
JSDOM (Vitestのデフォルト):
- メリット: 高速な実行。Node.js環境で動作するため、CI/CDパイプラインでのセットアップが比較的容易です。
- デメリット: 実際のブラウザの動作を完全に再現できないため、JSDOMでパスしたテストが実際のブラウザで失敗する「偽陽性」が発生する可能性があります。特にインタラクションやスタイリングのテストでは注意が必要です。
-
ブラウザ環境 (Vitest Browser Mode / Storybook Vitest Addon):
- メリット: 実際のブラウザでテストを実行するため、より高い信頼性でUIの動作を検証できます。特にインタラクションテストやビジュアルテストで真価を発揮します。
- デメリット: JSDOMに比べて実行速度が遅くなる可能性があります。Playwrightなどのブラウザプロバイダーのセットアップが必要になります。
-
JSDOM (Vitestのデフォルト):
-
単一のテストランナー vs. 複数のテストランナー:
-
Storybook + Vitestのみ:
- メリット: テストツールを統一することで、学習コストや設定の複雑さを軽減できます。
- デメリット: ユニットテストやE2Eテストなど、異なるテストの目的に対して最適なツールではない可能性があります。
-
Vitest (ユニットテスト) + Storybook (コンポーネントテスト) + Playwright (E2Eテスト):
- メリット: 各テストの目的に特化した最適なツールを使用できるため、テストの品質と効率が向上します。
- デメリット: 複数のツールを管理する必要があり、設定やCI/CDパイプラインの複雑さが増します。
-
Storybook + Vitestのみ:
UIテストのベストプラクティス
-
ストーリーをテストケースとして活用する: Storybookのストーリーは、コンポーネントの様々な状態を表現するテストケースとして機能します。これらのストーリーに
play関数を追加し、インタラクションとアサーションを記述することで、UIテストを効率的に自動化できます。 -
@storybook/addon-vitestを使用する: StorybookのVitestアドオンは、ストーリーをVitestテストに自動変換し、ブラウザ環境で実行できるため、コンポーネントテストのセットアップと実行を大幅に簡素化します。 - Vitest Browser Modeを活用する: 実際のブラウザでテストを実行することで、JSDOMの制限を回避し、より信頼性の高いUIテストを実現します。
-
インタラクションテストを重視する:
play関数と@storybook/testのユーティリティ (userEvent,expect,within) を使用して、ユーザーの操作をシミュレートし、コンポーネントの動作を検証します。これにより、コンポーネントが期待通りに機能するかを確認できます。 -
アクセシビリティテストを組み込む:
@storybook/addon-a11yを使用して、アクセシビリティの問題を自動的にチェックします。これにより、すべてのユーザーにとって使いやすいUIを保証できます。 - ビジュアルリグレッションテストを検討する: StorybookとChromaticなどのツールを連携させることで、UIの視覚的な変更を自動的に検出し、意図しないデザインの崩れを防ぎます。
- CI/CDパイプラインに統合する: StorybookとVitestのテストをCI/CDパイプラインに組み込むことで、コード変更ごとにUIコンポーネントの信頼性を自動的に検証し、デプロイ前に問題を検出できます。
- テストの分離: Vitest 4.0以降では、Storybookテストと他のユニットテストを分離するために、Vitestの「テストプロジェクト」機能を使用することが推奨されます。これにより、それぞれのテストタイプに最適な設定を適用できます。
-
依存関係の最適化: Viteの
optimizeDeps.includeを適切に設定し、テスト実行中の依存関係の再最適化による問題を回避します。
まとめ:StorybookとVitestでUIテストを自動化し、開発プロセスを加速する
本記事では、StorybookとVitestを連携させることで、UIコンポーネントのテストを効率的に自動化し、品質と開発体験を向上させる具体的な方法を解説しました。
重要なポイントは以下の通りです。
- Storybookのストーリーをテストケースとして活用し、
play関数でユーザーインタラクションとアサーションを記述することで、UIテストの自動化を実現できます。 -
@storybook/addon-vitestとVitestのBrowser Modeを組み合わせることで、実際のブラウザ環境での高信頼性なUIテストが可能です。 -
vitest.config.tsでテストプロジェクトを適切に設定し、ユニットテストとコンポーネントテストを分離することで、効率的なテスト運用が実現します。 - Viteの依存関係最適化やReactのバージョンミスマッチなど、導入時によくあるエラーに対する具体的な回避策を理解しておくことが重要です。
- CI/CDパイプラインへの統合により、コード変更ごとにUIコンポーネントの品質を保証し、リリースサイクルを加速できます。
StorybookとVitestの連携は、UIコンポーネントの品質保証と開発効率向上に大きく貢献します。ぜひ、本記事を参考に、あなたのプロジェクトに導入してみてください。
さらなる詳細や最新情報については、Storybook公式ドキュメントとVitest公式ドキュメントを参照することをお勧めします。