pnpm workspace + Vitest + Storybook でモノレポのコンポーネントテストを統一する
こんにちは。PKSHA Technology で SWE をしている須藤です。
私の所属する PKSHA Speech Insight(PSI)チームでは、pnpm workspace を用いたモノレポ構成で複数の Web アプリケーションを開発しています。
今回は、モノレポで Storybook を運用するための構成を紹介します。
TL;DR
- モノレポのルートに Storybook を 1 つ配置し、全パッケージの story を集約できる
-
@storybook/addon-vitestを使うと、Storybook を起動せずにplay関数をテストできる - CLI で高速にテスト実行できるので、AI エージェントにテスト→修正を任せやすい
- pnpm catalog でバージョンを一元管理すると、依存の重複問題を防げる
モチベーション
モノレポで複数の Web アプリケーションを開発していると、コンポーネントテストの管理が課題になります。
- 各パッケージで個別に Storybook を立ち上げるのは面倒
-
play関数を実行するために毎回ブラウザを開くのが手間 - パッケージ間で依存ライブラリのバージョンがずれてバグの原因になる
構成の概要
モノレポで Storybook を運用する場合、各パッケージに配置する方法もありますが、設定の重複やポート管理が煩雑になります。本記事では ルートに Storybook を 1 つ配置 し、Vitest と組み合わせてこれらの課題を解決した構成を紹介します。
| 配置方法 | メリット | デメリット |
|---|---|---|
| ルートに 1 つ | 設定の一元管理 CI や Chromatic 連携が容易 |
stories の glob が複雑になりがち |
| 各パッケージに配置 | パッケージの独立性が高い | 設定の重複、ポート管理が煩雑 |
技術スタック
- pnpm workspace + catalog
- Storybook 10 (
@storybook/addon-vitest) - Vitest 4 + Playwright
ディレクトリ構成
/
├── .storybook/ # ルートに Storybook を配置
│ ├── main.ts
│ ├── preview.tsx
│ └── vitest.setup.ts
├── packages/
│ ├── app-a/
│ │ └── src/
│ │ └── features/
│ │ └── SomeFeature/
│ │ ├── index.tsx
│ │ └── stories.tsx
│ ├── app-b/
│ └── shared-components/ # 共通コンポーネント
├── vitest.config.ts
├── pnpm-workspace.yaml
└── package.json
なぜ Vitest + Storybook なのか
Storybook 8.3 で Vitest 統合が導入され、story を Vitest のテストとして実行できるようになりました。
主なメリットは以下の通りです。
- story はテストに変換されるため、Storybook を起動せずにテスト実行可能
- ユニットテストと Storybook テストを同じ設定で管理できる
私たちも以前は Jest ベースの @storybook/test-runner を使用していましたが、Storybook 9 のリリースを機に移行しました。Jest と Vitest が混在する複雑さが解消され、設定がシンプルになりました。
テスト実行コマンド
package.json に以下のスクリプトを定義しておきます。
{
"scripts": {
"test-storybook": "vitest --project=storybook"
}
}
この構成の最大のメリットは、テスト実行がシンプルかつ高速なことです。
# 全パッケージの Storybook テストを実行
pnpm test-storybook
# 特定のファイルだけ実行
pnpm test-storybook run packages/app-a/src/features/SomeFeature/stories.tsx
# watch モードで実行
pnpm test-storybook --watch
Storybook を起動せずに、コマンド一発で全パッケージの story をテストできます。
CLI で完結するため、AI エージェント(Claude Code など)との相性も良く、テストの実行からデバッグ、修正までを高速に回せます。
設定ファイルの解説
この構成で使用する設定ファイルは以下の 3 つです。
pnpm-workspace.yaml
モノレポでは、パッケージ間で依存ライブラリのバージョンがずれると問題を引き起こすことがあります。例えば react のバージョンが異なると、複数のインスタンスが存在してしまい、hooks が正しく動作しなかったり Context が共有されないといった問題が発生します。
pnpm の catalog 機能を使うと、バージョンを一箇所で管理できます。
# pnpm-workspace.yaml
packages:
- packages/*
catalog:
# Storybook
storybook: ^10.1.11
"@storybook/addon-vitest": ^10.1.11
"@storybook/react-vite": ^10.1.11
# Vitest
vitest: ^4.0.15
"@vitest/browser": ^4.0.15
"@vitest/browser-playwright": ^4.0.15
# React
react: ^19.2.3
react-dom: ^19.2.3
各パッケージの package.json では catalog: で参照するだけです。
{
"devDependencies": {
"vitest": "catalog:",
"storybook": "catalog:"
}
}
vitest.config.ts
@storybook/addon-vitest を使って、Storybook の story を Vitest でテストします。
import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'
import { playwright } from '@vitest/browser-playwright'
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
// projects でユニットテストと Storybook テストを分離
projects: [
{
// ルートの test 設定を継承
extends: true,
plugins: [
storybookTest({
configDir: '.storybook',
}),
],
test: {
name: 'storybook',
// Playwright でブラウザ上でテストを実行
browser: {
enabled: true,
provider: playwright(),
instances: [{ browser: 'chromium', headless: true }],
},
setupFiles: ['.storybook/vitest.setup.ts'],
},
},
],
},
})
ポイントは Vitest の projects 機能を使っていることです。これにより、ユニットテスト(node 環境)と Storybook テスト(Playwright 環境)を分離できます。
.storybook/main.ts
ルートの Storybook で、全パッケージの story を一元管理します。
import { createRequire } from 'node:module'
import { dirname, join } from 'node:path'
import type { StorybookConfig } from '@storybook/react-vite'
// pnpm 環境でのモジュール解決用ヘルパー
// https://storybook.js.org/docs/faq#how-do-i-fix-module-resolution-in-special-environments
function getAbsolutePath(value: string): string {
const require = createRequire(import.meta.url)
return dirname(require.resolve(join(value, 'package.json')))
}
const config: StorybookConfig = {
// 各パッケージの story をルートで集約
stories: [
'../packages/app-a/src/**/stories.@(js|jsx|ts|tsx)',
'../packages/app-b/src/**/stories.@(js|jsx|ts|tsx)',
'../packages/shared-components/src/**/stories.@(js|jsx|ts|tsx)',
],
addons: [
getAbsolutePath('@storybook/addon-vitest'),
],
framework: getAbsolutePath('@storybook/react-vite'),
}
export default config
stories 配列で各パッケージの story ファイルへのパスを指定することで、ルートの Storybook に全パッケージの story を集約できます。
まとめ
この構成なら pnpm test-storybook で全パッケージを一括テストでき、Storybook の起動も不要です。pnpm catalog によるバージョン統一でパッケージの重複問題も防げます。
Storybook の play 関数を活用したインタラクションテストを書いている方は、ぜひこの構成を試してみてください。
本記事で紹介した構成のサンプルリポジトリを作成しました。興味のある方は参考までにご覧ください。
参考
Discussion