執筆

中川 幸哉

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

「たった1日で基本が身に付く! Androidアプリ開発超入門」(技術評論社) 「基礎から学ぶ React Native入門」(翔泳社)

監修

山田 祥寛

静岡県榛原町生まれ。一橋大学経済学部卒業後、NECにてシステム企画業務に携わるが、2003年4月に念願かなってフリーライターに転身。Microsoft MVP for Visual Studio and Development Technologies。執筆コミュニティ「WINGSプロジェクト」代表。主な著書に『「独習」シリーズ』、『これからはじめるReact実践入門』、『改訂3版 JavaScript本格入門』、他。

「独習」シリーズ 「これからはじめるReact実践入門」 「改訂3版 JavaScript本格入門」 他著書多数

はじめに:「このコンポーネント、壊れてない?」

「ちょっとリファクタしただけなのに、なぜか別の画面が壊れた...」

コンポーネント設計を丁寧に進めてきたつもりでも、変更の影響がどこまで波及するか分からない不安はつきものです。前回までで状態管理の設計を整理してきましたが、「設計が正しく動くことをどう保証するか」という問いには、まだ答えていません。

今回は、Testing Trophy(テスティングトロフィー)の考え方でテスト戦略を整理し、Storybookを使ったコンポーネントテストの実践について解説します。

Testing Trophy戦略に基づくコンポーネントテストの位置づけ

テスト戦略を語る上で、まず「テストピラミッド」という概念が有名です。単体テストを土台として大量に書き、統合テスト、E2Eテストと上に行くほど少なくするという考え方です。しかし、UIコンポーネント開発において、このモデルは必ずしも適切とは言えません。

Kent C. Dodds氏が提唱したTesting Trophyモデルでは、ピラミッドの中段にあたる統合テスト(Integration Tests)を最も重視します。コンポーネントのテストは、関数の動作を細かく確認する単体テストよりも、「ユーザーが実際に操作したときに、UIが期待通りに動くか」を確認する観点が重要だからです。

▲テストピラミッドとTesting Trophyの比較

ピラミッドと異なり、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要素を扱えます。ここでいう「ロール」とは、buttoncheckboxtextboxのように、その要素が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: 8pxpadding: 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はサーバー・クライアント境界という新たな設計軸をもたらし、今回学んだテスト戦略にも影響を与えます。