フロントエンド開発において、自動テストを書くことは品質を担保するために欠かせません。しかし、「何を単体テストで書き、何を結合テストで書くべきか」という境界線に迷うことはないでしょうか?
この記事では、Vue.js (Composition API) + TypeScript + Vitest で構築した「タスク管理アプリ (Task Manager)」の実装を題材に、単体テスト と 結合テスト の違いと、それぞれの役割、そして現場で求められる実践的な使い分けについて解説します。
実際に作ったタスク管理アプリ:
https://github.com/sisisa/test-learn
1. 単体テストとは?
単体テストは、アプリを構成する 「最小の部品(関数やクラス)」が、単独で正しく動くか を確認するテストです。
UIの描画、データベース、外部API通信などの他の部品には一切依存せず、「Aという入力を入れたら、絶対にBという出力が返ってくるか」だけを純粋に検証します。
このアプリでの具体例
タスク管理アプリにおける、タスクのタイトルをチェックする関数 validateTitle のテストがわかりやすい例です。
// tests/unit/validator.spec.ts より抜粋
describe('validateTitle', () => {
it('有効なタイトルでundefined(エラーなし)を返す', () => {
expect(validateTitle('買い物リスト')).toBeUndefined()
})
it('空文字でエラーメッセージを返す', () => {
// UIがどうなっているかは関係なく、関数に空文字を渡した結果だけを見る
expect(validateTitle('')).toBe('タイトルは必須です')
})
})
単体テストの特徴と役割
- 速い・確実: ブラウザを起動したり画面を描画したりしないため、一瞬で終わります。
-
原因箇所の特定が容易: もしこのテストが落ちたら、「
validator.tsのvalidateTitle関数がおかしい」とすぐに特定できます。 - 実務での役割: 「日付の計算」「金額の消費税計算」「パスワードの文字数チェック」など、絶対に間違ってはいけない コアなビジネスロジック をガチガチに守るために書きます。
2. 結合テストとは?
結合テストは、単体テストで安全が確認された複数の部品を 組み合わせて(結合して)、連携して正しく動くか を確認するテストです。
フロントエンド開発の場合、「画面(UI)」と「裏側のロジック」が意図通りに連動しているかを見るのが主な役割になります。
このアプリでの具体例
タスク追加フォームコンポーネント (src/components/TaskForm.vue) のテストがまさにこれに当たります。Vue Test Utilsを使ってユーザーの操作をシミュレートします。
// tests/integration/TaskForm.spec.ts より抜粋
it('空のまま送信するとタイトルエラーが表示される', async () => {
// 1. フォームコンポーネントを仮想的に画面に描画する
const wrapper = mount(TaskForm)
// 2. ユーザーが「追加する(送信)」ボタンを押した操作をシミュレート
await wrapper.find('[data-testid="task-form"]').trigger('submit')
// 3. 画面上に「タイトルは必須です」というエラー文言が表示されたか確認
expect(wrapper.find('[data-testid="error-title"]').text()).toContain('必須')
})
このテストの裏側では、以下のように複数の部品が連動して動いています。
- ユーザーがボタンを押す(UI操作)
-
useFormValidation.tsの制御が走る(Composableのロジック) -
validator.tsのvalidateTitle関数が呼ばれてエラーを返す(先ほどの単体テスト箇所) - エラー文言が画面に描画される(UIの更新)
結合テストの特徴と役割
- ユーザー目線: 「ボタンを押したらエラーが出る」「一覧が絞り込まれる」など、ユーザーが実際に体験するフローをテストします。
- 部品間のズレを検知: 「関数単体は正しいのに、画面に渡す変数を間違えていて表示されない」といった、結合時のミス(インターフェースの不整合)を防ぎます。
- 実務での役割: 「ログインフロー」「カートへの商品追加」「検索フィルターの動作」といった、ユーザーにとって重要な機能全体 が壊れていないかを担保するために書きます。
各テスト一覧:
| テスト種類 | 対象 | 目的 | このアプリでの対象 |
|---|---|---|---|
| 単体テスト | 最小部品(関数など) | 個々のロジックが完璧に正しいことの証明。 |
validator, formatter, taskFilter, taskApi など |
| 結合テスト | 組み合わせ(UI + ロジック) | ユーザーが画面を操作した際の一連の動きの証明。 |
TaskForm, TaskList, TaskItem など |
まとめ
実務において評価されるのは、「これらのテストの使い分けが論理的にできているか?」 という点です。
「すべてのテストを結合テスト(UI操作)でやろうとすると、テストの実行が遅くなりすぎてメンテナンスが破綻する」という事実があります。逆に「単体テストだけだと、つなぎ合わせた時に画面がちゃんと動く保証がない」という問題も生じます。
このアプリでは、「複雑なロジック(日付処理やバリデーション)は単体テストで高速かつ徹底的に叩き、全体が繋がったユーザー体験は結合テストで重要経路のみを抑える」 という、現場で最も評価される実用的なバランス(いわゆる「テストピラミッド」の考え方)を意図して構成しています。
これからテストを導入しようとしている方は、ぜひこの「関数のロジック担保」と「UIのユーザー体験担保」の使い分けを意識してみてください。