Claude Code で Vue 3 プロジェクトに Vitest 単体テストをゼロから整備した話
はじめに
Miyagi です。以前、Vue 2 → Vue 3 移行の計画と実施について記事を書きましたが、今回はその中でも テストケース作成 にフォーカスします。移行対象のプロジェクトにはテストが一切存在しなかったので、テスト基盤の構築からテストコードの作成まで、すべてゼロからの作業でした。
ゼロから約 30 ファイル・約 350 ケースのテストを書くのは、手作業でやると正直かなり腰が重い仕事です。特に API 通信のモック設計やストアテストの前準備は、一つのパターンを決めてしまえば横展開できるのに、その「最初のパターン」を作るまでが面倒。そこで Claude Code を使って、ガイドラインに沿ったテストコードを段階的に生成・レビューしながら整備しました。
本記事では以下について説明します:
- 概要
- テスト基盤の設計
- Claude Code に渡したプロンプトの設計
- 段階的なテスト作成の進め方
- テスト記述のガイドラインと品質管理
- 実際の成果と振り返り
- 今後の課題 ― コンポーネント層のテスタビリティ
- まとめ
概要
先に結論を書いておくと、「テスト記述のガイドラインを先に作り、それを Claude Code のプロンプトに含めて段階的に生成する」というアプローチがうまくいきました。 ガイドラインなしで「テスト書いて」とだけ指示すると、命名がバラバラだったりモック戦略が一貫しなかったりするので、事前にルールを固めておくのが肝です。
テストの技術スタックはこんな感じです。
| 項目 | 技術選定 |
|---|---|
| テストランナー | Vitest |
| DOM 環境 | happy-dom |
| HTTP モック | MSW v2 |
| ストアテスト | @pinia/testing |
| コンポーネントテスト | @vue/test-utils |
| カバレッジ | @vitest/coverage-v8 |
テスト対象は コンポーネント以外のロジック層に絞りました。API 通信モジュール、Pinia ストア、バリデーションルール、ユーティリティ関数、定数定義の 5 層です。コンポーネントテストは Playwright の E2E テストでカバーする方針にしたので、Vitest では「見た目」ではなく「ロジック」のテストに集中しています。
テスト基盤の設計
vitest.config.js
Vitest の設定は vite.config.js をマージして使う形にしました。Vite のエイリアス(@ → src/)やプラグイン設定をテストでもそのまま使えるので、設定の二重管理を避けられます。
import { mergeConfig } from 'vite'
import { defineConfig } from 'vitest/config'
import viteConfig from './vite.config'
export default mergeConfig(viteConfig, defineConfig({
test: {
globals: true,
environment: 'happy-dom',
include: ['tests/unit/**/*.test.js'],
setupFiles: ['tests/unit/setup.js'],
},
}))
DOM 環境は jsdom ではなく happy-dom を選びました。jsdom より軽量で、このプロジェクトのテスト(DOM 操作がほぼない純ロジックテスト)には十分です。実行速度も体感で速く、約 350 ケース全件が数秒で完了します。
MSW によるモック基盤
API 通信のモックには MSW(Mock Service Worker)を採用しました。axios の内部実装をモックする axios-mock-adapter と迷いましたが、MSW のほうが「HTTP レベルでインターセプトする」ため、axios の設定(インターセプターやベース URL)も含めてテストできます。
// tests/unit/setup.js
import { server } from './mocks/server.js'
beforeAll(() => server.listen({ onUnhandledRequest: 'warn' }))
beforeEach(() => {
document.cookie = 'oauthToken=test-token;path=/;'
})
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
setup.js で MSW サーバーのライフサイクルを管理し、各テストの前に認証トークンの Cookie をセットしています。onUnhandledRequest: 'warn' にしているので、ハンドラーを定義し忘れた API 呼び出しがあればコンソールに警告が出ます。
ハンドラーは tests/unit/mocks/handlers/ ディレクトリに API モジュール単位で配置し、handlers.js で統合する構成です。
テストディレクトリ構成
tests/unit/
├── setup.js # グローバルセットアップ(MSW)
├── fixtures/ # テストデータ
├── helpers/ # ヘルパー(Pinia セットアップ等)
├── mocks/ # MSW ハンドラー
│ ├── server.js
│ ├── handlers.js
│ └── handlers/ # API モジュール単位
└── src/ # テスト対象のディレクトリ構造に対応
├── api/
├── const/
├── plugins/
├── store/
└── utils/
tests/unit/src/ 配下は、本体の src/ と同じディレクトリ構造にしています。テストファイルを探す時に「元のファイルのパスがそのまま分かる」のが利点です。
Claude Code に渡したプロンプトの設計
ここが今回の記事で一番伝えたいところです。Claude Code に「テストを書いて」とだけ頼んでも、それなりのテストは出てきます。でも、約 30 ファイルにわたるテストの一貫性を保つには、プロンプトにガイドラインを埋め込む必要がありました。
プロンプトの構成
Claude Code の新規セッション用に、以下の構成のプロンプトを用意しました。
- プロジェクトのコンテキスト ― 技術スタック、ディレクトリ構造、テスト対象ファイルの一覧
- テスト記述ガイドライン ― 命名規則、describe/it の構造、モック戦略
- 段階的な作業指示 ― 「まず utils から始めて、次に validation、その次に API…」という順序
- 品質基準 ― カバレッジ目標、レビュー観点
重要なのは、ガイドラインを別ファイルとして用意しておき、プロンプトから参照させたことです。ガイドラインが長すぎるとプロンプト本文が読みにくくなりますし、ガイドライン自体も独立して改善できるので。
実際に使ったプロンプトの骨格はこんな感じです(プロジェクト固有の部分は省略しています)。
# ユニットテスト作成
## コンテキスト
- テストランナー: Vitest / DOM: happy-dom / HTTP モック: MSW v2
- テスト対象: src/utils/, src/plugins/, src/api/, src/store/
## 作業手順
1. まず以下のファイルを読んでください
- テスト記述ガイドライン: tests/unit/README.md
- テスト対象ファイル: src/utils/dateFormat.js
2. ガイドラインに従い、テストファイルを作成してください
3. 作成後、ガイドラインに準拠しているかセルフレビューしてください
## 品質基準
- カバレッジ 100% を目標
- 正常系・異常系・エッジケースを網羅
ガイドラインの要点
プロンプトから参照させたガイドラインは、大きく分けて「命名と構造」「モック戦略」「テスト観点」の 3 つを定義しています。要点だけ抜粋すると:
命名と構造:
- ファイル名は
{対象ファイル名}.test.js - it の文言は「〜の場合、〜になる」形式(例:
it('401が返った場合、ログアウト処理が呼ばれる')) - describe の第一階層に
/** */で観点サマリを記載 - 第二階層は「正常系」「異常系」「エッジケース」「境界値」で分類
モック戦略:
- API 通信は MSW を基本とする
vi.mock()は MSW で対応できない場合のみ(モジュール差し替え等)
テスト観点(API 層の例):
- 正常系のレスポンス変換
- 400 / 401 / 404 / 500 の各エラーハンドリング
- ネットワークエラー(レスポンスなし)
- 空データ / null のエッジケース
このガイドラインがあることで、Claude Code が生成するテストのばらつきがかなり抑えられました。最初の数ファイルでガイドライン自体の不備にも気づけたので、早い段階で作っておいて正解でした。
プロンプト設計で意識したこと
「何を書くか」だけでなく「どう書くか」を指定する。 たとえば「バリデーションルールのテストを書いて」ではなく、「正常系・異常系・境界値の3層構造で、it の文言は『〜の場合、〜になる』形式で書いて」と指定します。
テスト対象のファイルを先に読ませる。 Claude Code にはファイル読み込みの指示を出し、実装コードを理解させた上でテストを書かせます。「このファイルの全関数に対してテストを書いて」と言えば、export されている関数を全てカバーしてくれます。
一度に全部やらせない。 後述する段階的アプローチと連動しますが、「まず utils の数ファイルだけ」「次にバリデーションの 1 ファイル」と区切ることで、各ステップでレビューと修正が入れられます。
段階的なテスト作成の進め方
テスト対象を優先度で分類し、簡単なものから着手しました。「ベストよりベターファースト」が方針です。
ステップ 1: ユーティリティテスト
最初に手をつけたのは utils/ 配下の純粋関数です。日付フォーマット変換、ファイル操作ヘルパー、汎用ユーティリティなど。外部依存がほぼなく、入力と出力の対応が明確なので、テスト基盤の動作確認も兼ねて最適でした。
ここで基盤が正しく動くことを確認してから、次のステップに進みます。
ステップ 2: バリデーションルール
vee-validate 4 のカスタムルール(defineRule で登録されるもの)のテストです。メール、パスワード、最小・最大文字数、ファイルサイズなど。ルール数が約 30 種類あり、それぞれに正常系・異常系・境界値を書くので、ケース数が最大になりました。
Claude Code にはルール定義ファイルを読ませた上で、「各ルールに対して境界値テストを含めて書いて」と指示しました。min-1 / min / max / max+1 のパターンは Claude Code が得意とするところで、一発でかなり良いテストが出てきます。人間がやると境界値を漏らしがちですが、AI は網羅的に出してくれるので助かりました。
ステップ 3: API モジュール
API 通信層のテストです。ここからは MSW のハンドラー定義も同時に作る必要があるので、やや複雑になります。
API 層には「各モジュール」と「共通インターセプター」の 2 種類のテストがあります。各モジュールはリクエストの組み立て(正しいエンドポイント・HTTP メソッド・パラメータ)を検証、共通インターセプターはエラーコード別のハンドリングを検証します。
インターセプターのテストは一番設計に悩みました。エラーコードの分岐が多く(認証エラー、バリデーションエラー、Not Found、サーバーエラー、メンテナンス、ネットワークエラー…)、それぞれに対してエラーストアへのメッセージ設定やリダイレクト処理を検証する必要があります。最終的に「エラーコード別に describe を分ける」構造に落ち着きました。
ステップ 4: Pinia ストア
Pinia ストアのテストでは、API モジュールを vi.mock() でモック化し、ストアの action → state 更新の流れを検証します。MSW ではなく vi.mock() を使うのは、ストアテストでは「API の呼び出し結果に応じて state がどう変わるか」に集中したいからです。API 通信そのものの正しさは API 層のテストで検証済みなので、ここでは API の戻り値だけをモックします。
vi.mock('@/api/modules/messages', () => ({
messages: {
create: vi.fn(),
search: vi.fn(),
// ...
},
}))
ステップ 5: 定数テスト
正直なところ、定数ファイルのテストは後回しでも良かったのですが、カバレッジの底上げとエラーコードの正規表現マッチング検証のために書きました。「このエラーコードパターンがこの正規表現にマッチすること」「マッチしてはいけないパターンにマッチしないこと」を確認するテストは、地味ですがリグレッション防止に効きます。
テスト記述のガイドラインと品質管理
命名と構造のルール
テストの可読性を保つために、いくつかのルールを決めました。
it の文言は「〜の場合、〜になる」形式。 it('正しく動作する') は禁止です。何がどうなれば正しいのか分からないので。代わりに it('401が返った場合、ログアウト処理が呼ばれる') のように、条件と結果を明示します。テストが失敗した時に「何が壊れたか」がテスト名だけで分かるのが理想です。
describe は観点ベースで階層化。 第一階層が関数名やモジュール名、第二階層が「正常系」「異常系」「エッジケース」「境界値」。第一階層の直上にはブロックコメント /** */ で観点サマリを書きます。
/** ユーザー検索 API - リクエスト組み立て・レスポンス変換・エラーハンドリングを検証 */
describe('fetchUsers', () => {
describe('正常系', () => {
it('ページネーションパラメータが正しく送信される', () => { ... })
})
describe('異常系', () => {
it('404が返った場合、/404へリダイレクトされる', () => { ... })
})
})
テストデータの変数名は「なぜその値なのか」が伝わるように。 const input = { page: 0, limit: 5 } ではなく const firstPageParams = { page: 0, limit: 5 } です。テストコードは仕様書の側面もあるので、読んだ人がテストの意図を汲み取れる命名にしたかったのです。
モック戦略の使い分け
| 手法 | 使うとき |
|---|---|
| MSW | API 通信のモック(基本はこちら) |
vi.mock() |
モジュール単位の差し替え(ストアテストでの API モジュール等) |
vi.useFakeTimers() |
日付依存のテスト |
globalThis への代入 |
FileReader 等のブラウザ API モック |
MSW を基本にしたのは、テストが実装の詳細(axios のメソッド名やオプション)に依存しにくくなるからです。HTTP リクエストのレベルでモックするので、axios を fetch に置き換えても(理論上は)テストがそのまま通ります。
レビューと改善のサイクル
Claude Code が生成したテストコードは、毎回ガイドライン準拠のレビューを実施しました。具体的には以下の観点でチェックしています。
- it の文言が「〜の場合、〜になる」形式になっているか
- describe の構造が観点ベースになっているか
- テストデータの命名が意図を伝えているか
- MSW と vi.mock の使い分けが適切か
- カバレッジが目標に達しているか
このレビューも Claude Code に依頼しました。ガイドラインのファイルと生成されたテストコードを両方読ませて「このテストはガイドラインに準拠しているか確認して」と指示すると、違反箇所を指摘してくれます。人間のレビューの前段として、機械的なチェックを Claude Code に任せるのは効率が良かったです。
実際の成果と振り返り
テスト実行結果
| 項目 | 値 |
|---|---|
| テストファイル | 約 30 ファイル all passed |
| テストケース | 約 350 ケース all passed |
| 実行時間 | 数秒 |
カバレッジ
テスト対象として指定した全ファイルでほぼ 100% カバレッジを達成しました。副次的な成果として、カバレッジ計測の過程で本番コード内のデッドコードを発見できました。テストを書くこと自体がコード品質の可視化にもつながるという好例です。
テスト対象の優先度と実績
| 優先度 | 対象 | ファイル数 |
|---|---|---|
| P0(最優先) | 認証・バリデーション | 数ファイル |
| P1(高) | 共有ロジック(複数画面から利用) | 約 10 ファイル |
| P2(中) | ページ固有のAPI・ストア | 約 10 ファイル |
| P3(低) | 定数・変更頻度の低いコード | 約 10 ファイル |
振り返り
うまくいったこと:
ガイドラインを先に作ったのは正解でした。Claude Code に毎回「この形式で書いて」と指示するのではなく、ガイドラインファイルを参照させるだけで済むので、プロンプトがシンプルになります。ガイドライン自体も最初から完璧だったわけではなく、最初の数ファイルで気づいた問題点をフィードバックしながら改善しました。
段階的アプローチも良かったです。utils → validation → API → Store の順番で進めることで、各ステップで「前のステップの知見」が活きます。MSW のハンドラーを API テストで作り、それをストアテストでも再利用できたのは効率的でした。
改善したいこと:
最初のプロンプトでは、テスト対象ファイルの一覧を手動で列挙していました。これは面倒ですし、ファイルの追加・削除があると漏れます。今なら glob パターンで指定するか、Claude Code にディレクトリを走査させてテスト対象を自動検出させるほうが良いと思います。
あと、ストアテストの vi.mock() のボイラープレートが各ファイルで重複しています。共通ヘルパーに括り出すか悩みましたが、テストファイルは「そのファイルだけ読めば全体が分かる」状態のほうが保守しやすいので、あえて重複を許容しました。これは好みの問題かもしれません。
Note: ボイラープレート(boilerplate)とは、プログラミングにおいて「定型的に繰り返し書く必要があるコード」のことです。ここでは
vi.mock()のモック定義やインポート文など、各テストファイルで毎回ほぼ同じ形で記述する定型コードを指しています。
今後の課題 ― コンポーネント層のテスタビリティ
今回テスト対象にしたのは API 通信・ストア・バリデーション・ユーティリティといったロジック層で、Vue コンポーネント自体の単体テストはスコープ外としました。まずはロジック層のカバレッジを固めるのが先決という判断です。ただ、この過程でコンポーネント層のテスタビリティを高めるための次の一手も見えてきました。
現状、コンポーネントの中にビジネスロジックが直接埋め込まれている箇所がそれなりにあります。データの加工、条件分岐、API 呼び出しの組み合わせなどがテンプレートや methods に混在していて、テストしようとすると DOM のマウントやライフサイクルの再現が必要になり、テストが壊れやすく再現性も低くなります。
理想的には、コンポーネントから ロジックを Composable や純粋関数に抽出して、コンポーネント側は「受け取ったデータを表示する」「ユーザー操作を関数に渡す」だけの薄いレイヤーにしたいところです。そうすれば、抽出したロジックは Vitest で高速かつ安定したテストが書けますし、コンポーネント側は「正しい関数を呼んでいるか」「表示が意図通りか」という最小限のテストで済みます。
ただし、全画面を一気にリファクタリングするのはリスクが大きいので、まず 1 画面をパイロットとしてロジック抽出 → テスト作成のパターンを確立してから横展開する進め方が現実的だと考えています。
カバレッジの考え方もロジック層とコンポーネント層で分けたほうが良さそうです。抽出したロジック層は純粋関数なので 100% を狙う価値がありますが、コンポーネント層は主要なユーザー操作パスをカバーすれば十分で、v-if の全分岐を網羅するためだけに不自然なテストを書くのは保守コストに見合いません。数値を追いかけること自体が目的にならないよう、気をつけたいところです。
まとめ
テストゼロのプロジェクトに Vitest の単体テストを整備するのは、心理的にも技術的にもハードルが高い作業です。ただ、Claude Code をうまく使うことで「最初の一歩」のコストを大幅に下げられました。
ポイントをまとめると:
- ガイドラインを先に作る ― 命名規則・構造・モック戦略を文書化してからテストを書き始める
- ガイドラインをプロンプトに含める ― Claude Code に「どう書くか」まで指示する
- 簡単なものから段階的に ― utils → validation → API → Store の順で、基盤の動作確認を兼ねて進める
- レビューも Claude Code に依頼する ― ガイドライン準拠の機械的チェックは AI に任せ、人間は設計判断に集中する
- 完璧を目指さない ― 「ベストよりベターファースト」で、まず動くテストを作ってから改善する
- 次のステップはロジックの分離 ― コンポーネントからロジックを抽出し、テスタビリティと保守性を両立させる
最後まで読んでいただき、ありがとうございました!
この記事が少しでもお役に立てれば嬉しいです。良い開発ライフを!