

執筆
中川 幸哉
有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティ(代表:山田祥寛)に所属するテクニカルライター。新潟県上越市出身。会津大学コンピュータ理工学部卒業後、現在は新潟市に在住。ReactやAndroidを軸に、モバイルアプリ開発やWebサイト制作、Webメディア編集部の業務改善や、プログラミング技術記事の執筆等に携わっている。著書に『たった1日で基本が身に付く! Androidアプリ開発超入門』、『基礎から学ぶ React Native入門』

監修
山田 祥寛
静岡県榛原町生まれ。一橋大学経済学部卒業後、NECにてシステム企画業務に携わるが、2003年4月に念願かなってフリーライターに転身。Microsoft MVP for Visual Studio and Development Technologies。執筆コミュニティ「WINGSプロジェクト」代表。主な著書に『「独習」シリーズ』、『これからはじめるReact実践入門』、『改訂3版 JavaScript本格入門』、他。
はじめに:「このコンポーネント、壊れてない?」
「ちょっとリファクタしただけなのに、なぜか別の画面が壊れた...」
コンポーネント設計を丁寧に進めてきたつもりでも、変更の影響がどこまで波及するか分からない不安はつきものです。前回までで状態管理の設計を整理してきましたが、「設計が正しく動くことをどう保証するか」という問いには、まだ答えていません。
今回は、Testing Trophy(テスティングトロフィー)の考え方でテスト戦略を整理し、Storybookを使ったコンポーネントテストの実践について解説します。
Testing Trophy戦略に基づくコンポーネントテストの位置づけ
テスト戦略を語る上で、まず「テストピラミッド」という概念が有名です。単体テストを土台として大量に書き、統合テスト、E2Eテストと上に行くほど少なくするという考え方です。しかし、UIコンポーネント開発において、このモデルは必ずしも適切とは言えません。
Kent C. Dodds氏が提唱したTesting Trophyモデルでは、ピラミッドの中段にあたる統合テスト(Integration Tests)を最も重視します。コンポーネントのテストは、関数の動作を細かく確認する単体テストよりも、「ユーザーが実際に操作したときに、UIが期待通りに動くか」を確認する観点が重要だからです。

ピラミッドと異なり、TrophyではE2Eテストの下に統合テストが大きく位置します。コンポーネントが持つ状態遷移、Props経由のインターフェース、ユーザー操作への反応——これらは単体テストより「使う側の視点に近いテスト」で確かめた方が、品質保証としての価値が高いのです。
Testing Trophyでは、静的解析が土台に置かれている点も重要です。TypeScriptやESLint等で検出できる問題はそこで早めに防ぎ、テストではユーザー操作や状態遷移、コンポーネント間の連携といった「実行して初めて分かる振る舞い」に注力します。この前提があるからこそ、単体テストを増やし続けるのではなく、統合テストを厚くする考え方が成り立ちます。
Storybookによる実践的テスト
では、Testing Trophyが重視する統合テストを、実際にどう書くか。ここで登場するのがStorybookです。Storybookはコンポーネントを隔離して表示・操作できる環境を提供し、Trophyの各層と次のように対応づけられます。
| Trophyの層 | 主な担い手 | Storybookの関与 |
|---|---|---|
| 静的解析 | TypeScript / ESLint | 型安全なStory定義(CSF3 + satisfies)で補強 |
| 単体テスト | Vitest / Jest | Custom Hooksや純粋関数はStorybook外でテスト |
| 統合テスト | Testing Library 等 | Story + play関数 + @storybook/addon-vitest(=本稿の主戦場) |
| E2Eテスト | Playwright / Cypress | 画面横断シナリオは別途。VRTで補完も可能 |
以降では、この「統合テスト」層を中心に、Storyの書き方とplay関数による操作テストを見ていきます。
「UIカタログ」から「テスト基盤」へ
Storybookを「デザイナーにUIを見せるためのカタログツール」として使っているチームは多いでしょう。しかし現代のStorybookは、コンポーネントテスト基盤としての役割を強く打ち出しています。
Storybookの各Story(表示パターン定義)は、そのままテストケースとして実行できます。Vitest・Playwrightと統合することで、CIパイプラインにコンポーネントテストを組み込む実用的な基盤になります。UIカタログとテスト仕様を同じファイルで管理できるのも、保守性の面で大きなメリットです。
Story定義の基本:CSF3形式
Storybook(バージョン10)では、CSF3(Component Story Format 3)形式でStoryを記述します。コンポーネントを隔離して表示パターンを定義する、最小限のStory定義を見てみましょう。
import type { Meta, StoryObj } from '@storybook/react-vite'; import { fn } from 'storybook/test'; import { TaskItem } from './TaskItem'; // metaオブジェクト:このファイルの対象コンポーネントと共通設定を定義 const meta = { title: 'TaskItem', component: TaskItem, args: { onDelete: fn(), // コールバックをスパイとして設定 }, } satisfies Meta<typeof TaskItem>; export default meta; type Story = StoryObj<typeof meta>; // 個別のStory:表示パターンを定義 export const Default: Story = { args: { label: 'コンポーネント設計を学ぶ', }, };
satisfies Meta<typeof TaskItem>によりTypeScriptの型推論が効き、argsに渡せるPropsが補完されます。テストユーティリティはstorybook/testパッケージからインポートします(@storybook/testではありません)。
argsのfn()はスパイ関数を生成するヘルパです。通常の関数と同様にコンポーネントへ渡せますが、呼び出された回数や引数を内部で記録しており、後述のplay関数内でexpect(args.onDelete).toHaveBeenCalledWith(...)のように検証できます。コールバックPropsを持つコンポーネントをテストする際の基本道具です。
play関数によるInteraction Testing
Interaction Testing(インタラクションテスト)とは、クリックや入力といったユーザー操作をシミュレートし、コンポーネントが期待通りに反応するかを検証するテストです。Storyのテスト機能の核心となるのがplay関数で、Storyが表示された後に実行される操作シナリオを記述し、期待する状態変化を検証します。
import { expect, userEvent, within } from 'storybook/test'; // チェックで完了状態になることを確認するStory export const ToggleDone: Story = { args: { label: 'コンポーネント設計を学ぶ', }, play: async ({ canvasElement }) => { // canvasElement:Storyのレンダリング領域 const canvas = within(canvasElement); // aria属性を使ってUI要素を特定(実装の詳細に依存しない) const checkbox = canvas.getByRole('checkbox', { name: /完了にする/ }); await expect(checkbox).not.toBeChecked(); // ユーザー操作をシミュレート await userEvent.click(checkbox); await expect(checkbox).toBeChecked(); }, }; // 削除ボタンのコールバックが呼ばれることを確認するStory export const DeleteTask: Story = { args: { label: 'コンポーネント設計を学ぶ' }, play: async ({ canvasElement, args }) => { const canvas = within(canvasElement); const deleteButton = canvas.getByRole('button', { name: /削除/ }); await userEvent.click(deleteButton); // fn()で設定したスパイへの呼び出しを検証 await expect(args.onDelete).toHaveBeenCalledWith('コンポーネント設計を学ぶ'); }, };
getByRoleで要素を特定することで、アクセシビリティツリーに基づいてUI要素を扱えます。ここでいう「ロール」とは、button・checkbox・textboxのように、その要素がUI上どんな意味的役割を担っているかを示すラベルで、HTMLのタグやARIA属性から決まります。ロールベースで要素を探すことで、クラス名やDOM構造の細部に依存しにくいテストを書けます。これはTesting Trophyの「実装の詳細に依存しない」というテスト哲学と一致します。
VitestとCIへの統合
Storyのplay関数は、Vitestと統合することでコマンドラインから一括実行できます。Storybook 10では@storybook/addon-vitestを導入して設定すると、以下のコマンドでPlaywrightブラウザ上のコンポーネントテストを実行できます。
npx vitest run --project=storybook
CIでは別途Storybookサーバーを立ち上げなくても、VitestがStorybookの設定を読み込んでテストを実行できます。もっとも、そのためには@storybook/addon-vitestの登録やVitest側のbrowser mode(ブラウザモード)設定が済んでいる必要があります。
見た目の意図しない変化を検出するVisual Regression Testing
CSSの修正やUIライブラリのアップデートを行ったとき、触ったはずのない別画面でレイアウトが崩れてしまう——コンポーネント開発でよくある事故です。play関数は「操作した結果、状態が期待通りに変わるか」を見ますが、「ボタンの余白が2px広がった」「文字色が微妙に暗くなった」といった見た目の変化は検出できません。
ここで有効なのがVisual Regression Testing(VRT)です。Storyごとのスクリーンショットを事前に正解画像として保存しておき、変更後の画像とピクセル単位で比較することで、意図しない見た目の変化を自動検出します。すべてのStoryを一気に「再検品」できるため、影響範囲が見えにくいCSSやライブラリ更新にも踏み込みやすくなります。Chromaticなどのサービスを使えば、Storyをベースにこの比較をCI上で自動化できます。
たとえば、共通のButtonコンポーネントでpadding: 8pxをpadding: 12pxに変えただけのつもりが、TaskItemの行高まで変わり、一覧画面のレイアウトが微妙に崩れる——こうしたケースで、VRTは「意図したButtonのStoryだけでなく、それを使うTaskItemのStoryの画像も変わっています」と差分として提示してくれます。
VRTはInteraction Testingと相補的な関係にあります。ロジックの検証はplay関数で、スタイルの意図しない変化はVRTで、というように役割を分けて組み合わせると、コンポーネントの品質を多面的に保証できます。
テスタブルな設計は、良いコンポーネント設計でもある
ここで、本連載のテーマに立ち戻ってみましょう。第2回のコンポーネント分割、第3〜4回のフォルダ設計、第5〜6回のProps設計——これらの回で学んだ設計原則を適用したコンポーネントは、実はそのままテストしやすい構造になっています。
単一責務で小さく分割されたコンポーネントは、Storybookのコンポーネントテストやplay関数のシナリオをシンプルに保てます。Props経由のインターフェースが明確なコンポーネントは、argsで表示パターンをコントロールしやすくなります。Custom Hooksにロジックを切り出した設計では、フック単体をVitestのrenderHookでテストし、UI側はStory + play関数でテスト、というように独立して検証できます。
逆に、「テストが書きにくい」と感じる場面は、コンポーネントの責務が大きすぎる、あるいはPropsの依存が複雑すぎるというサインでもあります。テスタビリティは、設計品質のフィードバック指標として機能するのです。
まとめ
今回は、Testing TrophyによるテストのポジショニングとStorybookを活用したInteraction Testingの実践について解説しました。
Storybookは「UIカタログ」から「コンポーネントテスト基盤」へと進化しており、CSF3形式のStoryとplay関数、Vitestとの統合によって、CIで回せる実用的なテスト環境を構築できます。そして何より、テストを書きやすい設計は、これまで学んできたコンポーネント設計の原則を実践した結果として自然に得られるものです。
次回は、React Server Components(RSC)時代のコンポーネント設計について、批判的な視点も交えながら解説します。RSCはサーバー・クライアント境界という新たな設計軸をもたらし、今回学んだテスト戦略にも影響を与えます。


