📚

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 のテストとして実行できるようになりました。
https://storybook.js.org/docs/writing-tests/integrations/vitest-addon

主なメリットは以下の通りです。

  • 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 関数を活用したインタラクションテストを書いている方は、ぜひこの構成を試してみてください。

本記事で紹介した構成のサンプルリポジトリを作成しました。興味のある方は参考までにご覧ください。

https://github.com/Suto-Michimasa/monorepo-storybook-vitest

参考

https://storybook.js.org/blog/storybook-10/
https://storybook.js.org/docs/writing-tests/integrations/vitest-addon
https://pnpm.io/catalogs
https://buildersbox.corp-sansan.com/entry/2025/12/06/100000
https://zenn.dev/layerx/articles/19691b45fd261d

PKSHAテックブログ

Discussion