6
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

武藤と申します。
日本総合システム株式会社でシステム開発に従事しています。

前回の記事 では、フロントエンド開発に挑戦したい方に向けて

  • Vue を用いたフロントアプリケーションの構築
  • Vuetify を用いたコンポーネントの開発

について解説しました。

本記事では、フロントエンド開発をさらに勉強したい方に向けて

  • Storybook を用いたコンポーネントの開発と検証
  • Vitest を用いたコンポーネントのテスト自動化

について解説します。
ソースコードは GitHub で公開しています。

Storybook とは

コンポーネント開発のためのいわゆる【フロントエンド・ワークショップ】として、フロントアプリケーションを起動することなくコンポーネントを描画(レンダリング)することができるツールです。

Storybook を利用することで、コンポーネントの状態を【ストーリー】として記述し、その外観と動作を検証できるようになります。
それによって、UI をチームの共通言語にして認識を揃えることができ、開発者間のコミュニケーション・協働をスムーズにする効果が期待できます。

詳細は後述しますが、以下のようなファイルを作成します。拡張子は.stories.tsとなります。

ExampleComponent.stories.ts
import type { Meta, StoryObj } from '@storybook/vue3-vite'

import ExampleComponent from './ExampleComponent.vue'

const meta = {
  title: 'Example',
  component: ExampleComponent,
  render: (args: any) => ({
    components: { ExampleComponent },
    setup() {
      return { args }
    },
    template: '<ExampleComponent v-bind="args" />',
  }),
  argTypes: { ... },
  args: { ... },
  tags: ['autodocs'],
} satisfies Meta<typeof ExampleComponent>

export default meta
type Story = StoryObj<typeof meta>

export const Example1: Story = {
  args: { ... },
}

export const Example2: Story = {
  args: { ... },
}

Vitest とは

(「ヴィーテスト」と発音します)

Vite【フロントエンド・ビルドツール】と統合された、高速で軽量なテストフレームワークです。

Vitest を利用することで、プロジェクトに自動テストを導入し、コンポーネントの振る舞いや状態の変化を検証できるようになります。
それによって、リグレッション(回帰)テストとして変更に対する安全網を持つことができ、開発者が機能追加・改修に安心して取り組める効果が期待できます。

詳細は後述しますが、以下のようなファイルを作成します。拡張子は.spec.tsまたは.test.tsとなります。

src/__tests__/App.spec.ts
import { describe, it, expect } from 'vitest'

import { mount } from '@vue/test-utils'
import App from '../App.vue'

describe('App', () => {
  it('mounts renders properly', () => {
    const wrapper = mount(App)
    expect(wrapper.text()).toContain('You did it!')
  })
})

環境構築

  • OS:Windows 11
  • IDE:Visual Studio Code

Node.js のインストール

Node.js は JavaScript の実行環境 です。ウェブサイト からインストーラをダウンロードしてインストールしてください。
以下のコマンドでインストールを確認できます。

node --version
npm --version

Vue プロジェクトの構築

以下のコマンドで新規に Vue プロジェクトを構築します。

npm create vue@latest

コマンドラインでいくつか質問されるので、以下のように回答(選択)してください。

質問 回答(選択)
Project name (target directory): 任意の名前
Use TypeScript? Yes
Select features to include in your project: Vitest (unit testing), Prettier (code formatting)
Select experimental features to include in your project: none ※未選択
Skip all example code and start with a blank Vue project? Yes

本記事は学習目的のため、必要最小限の構成としています。

作成されたプロジェクトフォルダに移動した後、以下のコマンドで依存関係をセットアップしてください。

npm install

プロキシ環境下でインストールに失敗する場合は、以下のコマンドでプロキシを設定できます。

npm config --global set proxy http://proxy.xxxxx.co.jp:8080
npm config --global set https-proxy http://proxy.xxxxx.co.jp:8080

以下のコマンドで現在の設定を確認できます。

npm config --global list

Storybook の導入

以下のコマンドでプロジェクトに Storybook を導入します。作成されたプロジェクトフォルダ直下で実行してください。

npm create storybook@latest

コマンドラインで質問されるので、以下のように回答(選択)してください。

質問 回答(選択)
What configuration should we install? Minimal: Just the essentials for component development.

本記事は学習目的のため、必要最小限の【Minimal】を選択しています。
本格的に開発を行う場合は【Recommended】の選択をおすすめします。

本記事の執筆時点(2026年3月)では、本コマンドは以下のようにエラー終了します。

┌  storybook v10.2.19
│
■  Failed to build the preview
│
■  Error: Can not found environment context for client

Vue DevTools と Storybook のバージョン互換性に起因するエラーが発生するようです。
詳細は こちらのIssue を確認してください。

以下のコマンドで Vue DevTools をダウングレードすることにより、本エラーを回避できます。

npm install vite-plugin-vue-devtools@7.7.9

以下のコマンドで Storybook を起動します。

npm run storybook

自動でブラウザが起動するので(またはブラウザでhttp://localhost:6006/にアクセスし)、Storybook の UI が表示されることを確認してください。

  • 画面表示例
    alt text

Vitest の導入

Vitest は、先ほど Vue プロジェクトを構築した際に合わせて導入しています。
package.jsonには、Vitest を起動するスクリプト("test:unit")も定義されています。
開発環境でこのスクリプトを実行(npm run test:unit)した場合、Vitest は【Watch Mode】で起動します。

Vitest を Watch Mode で起動すると、ファイルの変更を検知して、関連するテストだけを自動で再実行してくれます。
また、失敗したテストの再試行や、テストファイルの絞り込みが対話的にできます。

テスト駆動開発(TDD)のような、素早くテストフィードバックを得て実装を進めていく開発スタイルには特に適しています。

本記事では、Watch Mode をオフにして Vitest を起動することとします。
package.jsonに以下の記述を追加してください。

  "scripts": {
    ...
+   "test": "vitest run",
    "test:unit": "vitest",
    ...
  },

以下のコマンドで Vitest を起動します。

npm run test

テストが実行され、成功することを確認してください。

  • 実行結果例
 RUN  v4.1.0

 ✓ src/__tests__/App.spec.ts (1 test) 15ms
   ✓ App (1)
     ✓ mounts renders properly 13ms

 Test Files  1 passed (1)
      Tests  1 passed (1)
   Start at  12:34:56
   Duration  1.42s (transform 56ms, setup 0ms, import 168ms, tests 15ms, environment 1.05s)

Vuetify の導入

本記事では、Vuetify【Vue アプリケーションのための UI ライブラリ】を利用します。
以下のコマンドで Vuetify をインストールしてください。

npm install vuetify@3.11.0
npm install --save-dev @mdi/font

まずは、アプリケーションに Vuetify を適用します。1
src/main.tsに以下の記述を追加・修正してください。

import { createApp } from 'vue'
import App from './App.vue'

+ import { createVuetify } from 'vuetify'
+ import * as components from 'vuetify/components'
+ import * as directives from 'vuetify/directives'
+ import 'vuetify/styles'
+ import '@mdi/font/css/materialdesignicons.css'

+ const vuetify = createVuetify({ components, directives })

- createApp(App).mount('#app')
+ createApp(App).use(vuetify).mount('#app')

続いて、Storybook に Vuetify を適用します。2
.storybook/に以下のファイルを作成してください。

.storybook/preview.tsに以下の記述を追加してください。

import type { Preview } from '@storybook/vue3-vite'

+ import { setup } from '@storybook/vue3'
+ import { withVuetifyTheme } from './withVuetifyTheme.decorator'

+ import { createVuetify } from 'vuetify'
+ import * as components from 'vuetify/components'
+ import * as directives from 'vuetify/directives'
+ import 'vuetify/styles'
+ import '@mdi/font/css/materialdesignicons.css'

+ setup((app) => {
+   const vuetify = createVuetify({ components, directives })
+   app.use(vuetify)
+ })

+ export const decorators = [withVuetifyTheme]

最後に、Vitest に Vuetify を適用します。3
vite.config.tsに以下の記述を追加してください。

export default defineConfig({
+   test: {
+     globals: true,
+     environment: 'jsdom',
+     server: {
+       deps: {
+         inline: ['vuetify'],
+       },
+     },
+   },
  ...
})

以上で環境構築は完了です!

実装概要

Storybook について

Storybook は、コンポーネントの見た目と振る舞いを検証するための開発ツールです。
Storybook のストーリーファイル(*.stories.ts)には、コンポーネントを【どの状態で表示させるか】を記述します。

Storybook の基本用語

  • メタ情報(Meta

    • ストーリーファイル全体の共通設定
    • 「どのコンポーネントを対象とするか」や「共通の初期引数」といった、全ストーリー共通の設定を記述します
    • 以下のような設定をメタ情報として記述します
      • カテゴリ名(title
      • 対象コンポーネント(component
      • 描画方法(render
      • プロパティの操作方法(argTypes
      • 共通する引数(args
  • ストーリー(StoryObj

    • コンポーネントのある1つの状態を表す定義
    • export const Example1: Story = { ... }の形式で複数定義し、メタ情報の設定を引き継ぎつつ差分を記述します
    • 以下のような状態をストーリーとして記述します
      • 通常(デフォルト)の状態
      • 非活性(disabled)の状態
      • 通常と異なるスタイルの状態

Vitest について

Vitest は、コンポーネントの振る舞いや状態の変化をコードで検証するためのテストフレームワークです。
Vitest のテストファイル(*.spec.ts / *.test.ts)には、コンポーネントが【どのように振る舞うべきか】を記述します。

Vitest の基本用語

  • テストスイート(describe

    • 複数のテストを1つのまとまりとしてグルーピングするためのブロック
    • describe()を使って、テストをコンポーネント単位や機能単位でまとめることで、読みやすく整理しやすくなります
  • テスト(it / test

    • コンポーネントや関数が「期待通りに動作するか」を確認するための処理
    • it()またはtest()を使って、1つの動作(振る舞い)を検証する単位として記述します
  • アサーション(expect

    • 実際の結果が「こうなっているはず」という期待値を記述するための関数
    • 例えば、expect(value).toBe(true)のように、値や動作の結果を確認します
  • マウント(mount

    • Vue コンポーネントを実際に描画してテストできるようにするための関数
    • 表示テキスト・属性・クラス・イベント発火など、コンポーネントを検証する際の基本手段です

it()test()は機能的に同一(エイリアス)です。コーディングスタイルで使い分ける(どちらかに統一する)のがおすすめです。

  • it:振る舞い駆動開発(BDD)のスタイルで、ユースケース・仕様を物語るように記述できます

    • it('should ...')のように、「主語(it)+ should ...」の流れで、英文のように仕様を記述したい場合に馴染みます
  • test:説明的かつ一般的な命名・表現で、処理の事実・動作を端的に記述できます

    • test('does ...')のように、「何をテストしているか」を淡々と記述したい場合に馴染みます

実装解説

ここからはソースコードを基に実装の要点を解説していきます。
コンポーネント・ストーリーファイル・テストファイルをセットで掲載しています。ぜひお手元で動作確認してみてください。

コンポーネント

今回は、一般的なボタンコンポーネントを取り上げます。
コンポーネントそのものの実装解説については、前回の記事 を確認してください。

src/components/btn/Button.vue
<script setup lang="ts">
import { computed } from 'vue'

export type ButtonProps = {
  /** テキスト */
  text: string
  /** 非活性フラグ */
  disabled?: boolean
  /** ボタンの重要度 */
  importance?: 'primary' | 'secondary' | 'tertiary'
}

const props = withDefaults(defineProps<ButtonProps>(), { importance: 'primary' })

// クリックアクションを通知
defineEmits(['click'])

// ボタンの重要度によりスタイルを変更
const variant = computed(() => {
  return props.importance === 'primary'
    ? 'elevated'
    : props.importance === 'secondary'
      ? 'outlined'
      : 'tonal'
})
</script>

<template>
  <div>
    <v-btn
      @click="$emit('click')"
      :text
      :disabled
      :variant
      :color="importance"
      :base-color="importance"
      min-height="48"
      min-width="96"
    ></v-btn>
  </div>
</template>

Storybook

まずは、ボタンコンポーネントの見た目と振る舞いを検証する Storybook の実装を紹介します。
先ほど説明した【メタ情報】【ストーリー】をどのように実装するのかに注目です。

src/components/btn/Button.stories.ts
import type { Meta, StoryObj } from '@storybook/vue3-vite'

import { fn } from 'storybook/test'

import Button, { type ButtonProps } from './Button.vue'

const meta = {
  title: 'Components/Button',
  component: Button,
  render: (args: ButtonProps) => ({
    components: { Button },
    setup() {
      return { args }
    },
    template: '<Button v-bind="args" />',
  }),
  argTypes: {
    text: { control: 'text', description: 'テキスト' },
    disabled: { control: 'boolean', description: '非活性フラグ' },
    importance: {
      control: 'select',
      options: ['primary', 'secondary', 'tertiary'],
      description: 'ボタンの重要度',
    },
  },
  args: {
    // クリックアクションを通知
    onClick: fn(),
  },
  tags: ['autodocs'],
} satisfies Meta<typeof Button>

export default meta
type Story = StoryObj<typeof meta>

export const Primary: Story = {
  args: {
    text: 'プライマリ',
    importance: 'primary',
  },
}

export const Disabled: Story = {
  args: {
    text: '非活性',
    disabled: true,
  },
}

export const Secondary: Story = {
  args: {
    text: 'セカンダリ',
    importance: 'secondary',
  },
}

export const Tertiary: Story = {
  args: {
    text: 'ターシャリ',
    importance: 'tertiary',
  },
}
画面表示と UI の説明を見る

alt text

  • UI の説明
    • 左のサイドバーから、各ストーリーファイルで作成したストーリーを選択できます
    • 下のコントロールパネルから、各プロパティの値を設定・変更できます
解説を閉じる

本実装はコンポーネントの状態をストーリーとして可視化するストーリーファイルの基本構成となります。
ポイントは次の2点です。

  1. メタ情報として共通設定を宣言

    • ストーリーファイルは、メタ情報の記述から始まります。「どのコンポーネントを、どのように、どんな前提で表示・操作するのか」を記述します
      • title:Storybook 上のカテゴリ/表示名を定義します
      • component:ストーリーの対象コンポーネントを定義します
      • render:各ストーリーで共通の描画方法を定義します
        • componentsでコンポーネント、setup()でプロパティ、templateで描画方法(HTML)を指定しています
      • argTypes:各プロパティのコントロール(UI 上の操作方法)を定義します
        • textプロパティはcontrol: 'text'で文字列入力、disabledプロパティはcontrol: 'boolean'で ON / OFF の選択、importanceプロパティはcontrol: 'select'で与えた選択肢の中から選択を指定しています
      • args:各ストーリーで共通の初期引数を定義します
        • onClick: fn()fn()は Storybook のモック関数)を指定し、クリックイベントの観測ができるようにしています
  2. ストーリーとして状態を列挙

    • ボタンコンポーネントの状態変化として、disabledプロパティにtrueを渡した場合とimportanceプロパティのバリエーションを列挙しています
    • 見た目(色・形など)を調整したり、プロパティに応じた振る舞いを確認するなど、コンポーネントの「生きた仕様書」として機能します

Vitest

続いて、ボタンコンポーネントの振る舞いや状態の変化を検証する Vitest の実装を紹介します。
先ほど説明した【テストスイート】【テスト】【アサーション】【マウント】をどのように実装するのかに注目です。

src/components/btn/Button.spec.ts
import { describe, it, expect } from 'vitest'

import { mount } from '@vue/test-utils'

import { createVuetify } from 'vuetify'
import * as components from 'vuetify/components'
import * as directives from 'vuetify/directives'

import Button, { type ButtonProps } from './Button.vue'

const vuetify = createVuetify({ components, directives })

describe('Button', () => {
  const mountTarget = (props: ButtonProps) => {
    return mount(Button, {
      props,
      global: { plugins: [vuetify] },
    })
  }

  it('テキストを表示できる', () => {
    const text = 'ボタン'
    const wrapper = mountTarget({ text })
    expect(wrapper.text()).toContain(text)
  })

  it('非活性フラグが有効な場合に非活性化できる', async () => {
    const wrapper = mountTarget({ text: '非活性', disabled: true })
    await wrapper.get('button').trigger('click')
    expect(wrapper.get('button').element.disabled).toBe(true)
    expect(wrapper.emitted('click')).toBeUndefined()
  })

  it('クリックアクションを通知できる', async () => {
    const wrapper = mountTarget({ text: 'クリック' })
    await wrapper.get('button').trigger('click')
    expect(wrapper.emitted('click')).toHaveLength(1)
  })
})
実行結果を見る
 RUN  v4.1.0

 ✓ src/__tests__/App.spec.ts (1 test) 18ms
 ✓ src/components/btn/Button.spec.ts (3 tests) 82ms

 Test Files  2 passed (2)
      Tests  4 passed (4)
   Start at  12:34:56
   Duration  4.60s (transform 2.54s, setup 0ms, import 3.45s, tests 100ms, environment 2.12s)
解説を閉じる

本実装は【表示】【状態】【イベント】というコンポーネントの責務をカバーするテストファイルの基本構成となります。
ポイントは次の4点です。

  1. マウント処理の共通化

    • テストごとに毎回mountおよび Vuetify のプラグイン設定を記述するのは冗長なので、共通のマウント処理関数mountTargetを作成しています
    • こうすることで、各テストはコンポーネントのプロパティに集中できるため、テストの可読性と保守性が向上します
  2. テキスト表示の検証

    • textプロパティに渡した文字列が描画されているかをwrapper.text()を用いて検証しています
    • 実装の詳細に依存せず、描画した結果を確認することで、壊れにくいテストになります
  3. 非活性状態の検証

    • disabledプロパティにtrueを渡した場合にボタンが非活性(disabled)になっているかをwrapper.get('button').element.disabledを用いて検証しています
    • さらに、ユーザがクリックしてもクリックイベントが発火(emit)しないことも合わせて確認しています
  4. クリックイベントの検証

    • wrapper.get('button').trigger('click')でユーザのクリック操作を模し、クリックイベントが発火(emit)するかをwrapper.emitted('click')を用いて検証しています
    • ユーザの操作から Vue のイベント発火という外部から内部への流れをカバーしています

その他の実装例

ボタンコンポーネント以外(テキストフィールド・チェックボックス等)の実装例は リポジトリ に配置しています。
コンポーネント・ストーリーファイル・テストファイルをセットで用意しています。こちらも合わせて確認してみてください。

おわりに

Storybook と Vitest によるコンポーネントの開発と検証、テスト自動化について解説しました。

開発が慌ただしくなってくると、ストーリーファイル・テストファイルの作成や更新は後回しにされがちです。
しかし、短期的には手間に感じられても 【未来への投資として捉える】 ことで、長期的には開発効率と品質の大幅な向上につながります。
チームとプロダクトをより強くするためにも、開発プロセスの中にきちんと組み込んで継続的にメンテナンスしていきましょう。

ご覧いただきありがとうございました。

  1. Vuetify の公式ドキュメント を参考にしています

  2. Storybook の公式ドキュメント を参考にしています

  3. Vuetify の公式ドキュメント を参考にしています

6
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?