5
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【React】Vitest + React Testing Library 入門 Part1 〜環境構築からテストの基礎まで〜

5
Posted at

フロントエンドテスト入門 Part 1 — テストとは何か、なぜFEエンジニアが学ぶべきなのか

この記事は「フレッシャーFEがテストをゼロから学ぶ」シリーズの第1弾です。


はじめに — なぜ私はテストを学ぼうと思ったのか

正直に言うと、フロントエンドエンジニアとしてしばらく働いてきて、「テスト」という言葉は知っていたけれど、ずっと後回しにしていました。

「UIは目で確認すればいいじゃないか」
「バックエンドの人がやるものじゃないの?」
「テスト書く時間があれば機能を実装したい」

こんな気持ちが正直ありました。でも、勉強を進めるうちに、テストはエンジニアとしての成長に直結するスキルだと気づきました。特にフレッシャーのうちに学んでおくと、後々すごく差がつきます。

この記事では、テストの基礎概念から始めて、なぜFEエンジニアにとってテストが重要なのか、どんなツールを使うのかを丁寧に解説します。コードの話は次回以降にして、今回はしっかり「考え方」を固めましょう。


テストとは何か — 一番シンプルな説明

テストとは、コードが期待通りに動くことを自動的に確認する仕組みです。

普段、私たちはブラウザを開いてボタンをクリックして「動いた!」と確認しますよね。それも一種のテストです。でも、手動テストには大きな問題があります。

  • 毎回手作業で確認しなければならない
  • 修正するたびに全機能を再確認する必要がある
  • 人間なのでミスをする
  • 時間がかかる

自動テストを書けば、コマンド一つで数百・数千のチェックが数秒で完了します。しかも毎回同じ品質で。

# このコマンド一つで全テストが走る
npm run test

テストの種類 — Unit / Integration / E2E の違い

フロントエンドのテストは大きく3種類に分けられます。それぞれ目的と対象が異なります。

1. ユニットテスト(Unit Test)

一番小さい単位のテストです。関数やコンポーネント一つひとつが正しく動くかを確認します。

例えば:

  • formatDate(date) という関数が正しい日付フォーマットを返すか
  • Button コンポーネントが正しくレンダリングされるか
  • バリデーション関数が正しくエラーを返すか

特徴:

  • 実行速度が速い(ミリ秒単位)
  • 書くのが比較的簡単
  • どこが壊れているか特定しやすい
  • 外部依存(APIなど)を使わない

ユニットテストはテストの基本中の基本です。このシリーズではここから始めます。

2. インテグレーションテスト(Integration Test)

複数のコンポーネントや機能が組み合わさったときの動作を確認するテストです。

例えば:

  • フォームに入力してSubmitしたとき、正しくAPIが呼ばれるか
  • ユーザーがログインフローを完了したとき、ダッシュボードに遷移するか
  • Zustandのstoreとコンポーネントが連携して正しく動くか

特徴:

  • ユニットテストより現実に近い
  • 少し遅い
  • 「部品ひとつひとつは正常でも、組み合わせると壊れる」問題を発見できる

3. E2Eテスト(End-to-End Test)

ユーザーが実際に操作するのと同じ流れを自動で再現するテストです。ブラウザを実際に起動して、クリックや入力を自動で行います。

例えば:

  • ブラウザを開く → ログインページにアクセス → メールとパスワードを入力 → ログインボタンをクリック → ダッシュボードが表示されることを確認

ツールとしては PlaywrightCypress が有名です。

特徴:

  • 最も現実に近い
  • 実行が遅い(数十秒〜数分)
  • 環境の影響を受けやすい
  • セットアップが複雑

テストピラミッドという考え方

テストの世界では「テストピラミッド」という考え方があります。

        /\
       /E2E\        ← 少ない(重くて遅い)
      /------\
     /  統合  \      ← 中くらい
    /----------\
   /  ユニット  \    ← たくさん(軽くて速い)
  /--------------\

ユニットテストを一番多く書き、E2Eは重要な部分だけに絞る、というのが基本的な考え方です。最初はユニットテストとインテグレーションテストから学ぶのがベストです。


なぜFEエンジニアにとってテストが重要なのか

「バックエンドならわかるけど、フロントエンドでもテストって必要なの?」という疑問はよくあります。答えは絶対に必要です。理由を一つずつ説明します。

理由1:リグレッション(退行バグ)を防ぐ

リグレッションとは、以前動いていた機能が、別の変更をしたことで壊れてしまうことです。

例えば、ボタンのスタイルを直そうとしてCSSを変えたら、なぜかフォームのSubmitが動かなくなった。こういうことは実際の開発でよく起きます。

テストがあれば、変更後にテストを実行することで、壊れた部分をすぐに検知できます。

理由2:リファクタリングへの恐怖がなくなる

コードを書いていると、「この部分、もっとキレイに書けるのに…でも動いてるから触らないでおこう」という状況になりがちです。

テストがあれば、リファクタリング後にテストを走らせて「全部グリーン!」となれば、安心して変更できます。テストは変更に対するセーフティネットです。

理由3:コードの品質が上がる

テストを書こうとすると、自然とテストしやすいコードを意識するようになります。テストしやすいコードは、責任が明確で、依存関係が整理されていて、シンプルです。つまり良いコードです。

「テストが書きにくい」と感じたら、それはコード設計を見直すサインでもあります。

理由4:ドキュメントとして機能する

良いテストコードは、そのコンポーネントや関数が何をすべきかを示しています。

it('ユーザーが無効なメールアドレスを入力したとき、エラーメッセージを表示する', () => {
  // このテスト名を読むだけで仕様がわかる
})

新しいメンバーがコードベースに入ってきたとき、テストを読めば「このコンポーネントはどう動くのか」を素早く理解できます。

理由5:採用市場での差別化

これはフレッシャーにとって特に重要な話です。

フロントエンドエンジニアのほとんどは、フレッシャーのうちテストを書いた経験がほとんどありません。もしあなたが「Vitest + React Testing Libraryでユニットテストとインテグレーションテストを書いた経験があります」と言えたら、それだけで他の候補者と大きく差がつきます。

特に日本の企業では、テストをちゃんと書けるFEエンジニアを求めているところが多いです。


テストを書かないとどうなるか — 実際のシナリオ

少し極端ですが、テストがない場合に何が起きるか想像してみましょう。

シナリオ:ECサイトのカート機能

あなたはカートに商品を追加する機能を実装しました。テストは書いていません。動作確認はブラウザで手動で行いました。

3週間後、同僚が決済ページのUIを改修しました。その変更は一見カート機能とは無関係に見えました。でも実は共通のstoreを使っていて、カートの数量が正しく更新されなくなっていました。

リリース後にユーザーからバグ報告が来ました。原因の特定に2時間かかりました。

テストがあった場合:
同僚が変更をpushしてCIが走ったとき、カート機能のテストが失敗します。「あ、ここが壊れた」とすぐわかります。修正は5分で終わります。


このシリーズで使うツール

Vitest

VitestはViteをベースにした高速なテストフレームワークです。JavaScriptのテストフレームワークとしては他にJestが有名ですが、最近のReactプロジェクト(特にViteを使っているもの)ではVitestが主流になってきています。

なぜVitestを選ぶのか:

  • Viteプロジェクトとの相性が抜群
  • 設定がほぼ不要でゼロコンフィグで始められる
  • JestとほぼAPIが同じなので、移行しやすい
  • 実行速度が非常に速い
  • ホットリロードに対応している(テストをウォッチモードで実行できる)

React Testing Library(RTL)

React Testing LibraryはReactコンポーネントをテストするためのライブラリです。

RTLの一番の特徴は「ユーザーの視点でテストを書く」という哲学です。

例えば、DOMの内部実装(classNameid)ではなく、ユーザーが実際に見ているもの(テキスト、ロール、ラベル)を使ってコンポーネントを取得します。

// ❌ 実装の詳細に依存したテスト(RTLが避けるべきとするもの)
const button = container.querySelector('.submit-btn')

// ✅ ユーザー視点のテスト(RTLが推奨するもの)
const button = screen.getByRole('button', { name: '送信する' })

この違いは最初は小さく見えますが、とても重要です。実装の詳細に依存したテストは、UIのリファクタリングをするたびに壊れてしまいます。RTLのアプローチならリファクタリングに強いテストが書けます。

Vitest + RTLの組み合わせ

この2つはセットで使うのが現在のReact開発のデファクトスタンダードになっています。

Vitest   → テストランナー(テストを実行する基盤)
RTL      → ReactコンポーネントをDOMにレンダリングしてテストするためのユーティリティ

環境セットアップ — インストールから最初のテスト実行まで

では実際に環境を作ってみましょう。既存のReact + Viteプロジェクトがある前提で進めます。

インストール

npm install -D vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom

各パッケージの役割:

パッケージ 役割
vitest テストランナー本体
@testing-library/react ReactコンポーネントをレンダリングするためのRTL本体
@testing-library/jest-dom toBeInTheDocument()などの便利なマッチャーを追加
@testing-library/user-event クリックや入力などのユーザー操作をシミュレート
jsdom ブラウザ環境をNode.js上で模擬する

vite.config.ts の設定

/// <reference types="vitest" />
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  test: {
    globals: true,        // describe, it, expect をimportなしで使える
    environment: 'jsdom', // ブラウザ環境をシミュレート
    setupFiles: './src/test/setup.ts', // テスト前に実行するセットアップファイル
  },
})

セットアップファイルの作成

src/test/setup.ts を作成します:

import '@testing-library/jest-dom'

これだけでOKです。toBeInTheDocument()toHaveValue() などの便利なマッチャーが使えるようになります。

package.json にスクリプトを追加

{
  "scripts": {
    "test": "vitest",
    "test:ui": "vitest --ui",
    "test:coverage": "vitest run --coverage"
  }
}

動作確認 — 最初のテストを書いてみる

セットアップが正しくできているか確認するために、超シンプルなテストを書いてみましょう。

src/test/sample.test.ts を作成:

describe('サンプルテスト', () => {
  it('1 + 1 は 2 である', () => {
    expect(1 + 1).toBe(2)
  })

  it('文字列が正しく結合される', () => {
    const result = 'Hello' + ' ' + 'World'
    expect(result).toBe('Hello World')
  })
})

実行してみます:

npm run test

こんな出力が出れば成功です:

✓ src/test/sample.test.ts (2)
  ✓ サンプルテスト (2)
    ✓ 1 + 1 は 2 である
    ✓ 文字列が正しく結合される

Test Files  1 passed (1)
Tests       2 passed (2)
Duration    234ms

テストコードの基本構造を理解する

テストを書くときに必ず使う構文を覚えておきましょう。

describe — テストのグループ化

describe('Button コンポーネント', () => {
  // このブロック内にButtonに関するテストをまとめる

  describe('クリックイベント', () => {
    // さらに細かくグループ化もできる
  })
})

describeは必須ではありませんが、テストを整理するために使います。ネストすることもできます。

it / test — 個別のテストケース

it('ボタンが正しくレンダリングされる', () => {
  // テストの中身
})

// itとtestは同じ意味
test('ボタンが正しくレンダリングされる', () => {
  // テストの中身
})

ittest はどちらを使っても構いません。it('...する', () => {}) の形が英語として自然に読めるので it を好む人が多いです。

expect — アサーション(検証)

expect(実際の値).マッチャー(期待する値)

よく使うマッチャー:

// 値の比較
expect(2 + 2).toBe(4)                        // 厳密に等しい
expect({a: 1}).toEqual({a: 1})               // オブジェクトの中身が等しい
expect('Hello World').toContain('Hello')     // 文字列を含む
expect([1, 2, 3]).toHaveLength(3)            // 配列の長さ

// 真偽値
expect(true).toBeTruthy()
expect(false).toBeFalsy()
expect(null).toBeNull()
expect(undefined).toBeUndefined()

// DOM(jest-domを入れた場合)
expect(element).toBeInTheDocument()          // DOMに存在する
expect(element).toBeVisible()               // 見えている
expect(element).toHaveTextContent('テキスト') // テキストを持つ
expect(input).toHaveValue('入力値')           // 値を持つ
expect(button).toBeDisabled()               // disabledである

AAAパターン — テストの書き方の基本

テストは Arrange(準備)→ Act(実行)→ Assert(検証) の3ステップで書くのが基本です。

it('ユーザー名を表示する', () => {
  // Arrange — テストの準備
  const user = { name: '田中太郎', age: 25 }

  // Act — テスト対象の操作を実行
  const result = formatUserName(user)

  // Assert — 結果を検証
  expect(result).toBe('田中太郎 (25歳)')
})

このパターンを意識するだけで、読みやすいテストが書けるようになります。


テストファイルの命名規則

テストファイルの命名にはいくつかの慣習があります。

// パターン1:テスト対象ファイルと同じ場所に置く
src/
  components/
    Button.tsx
    Button.test.tsx    ← これが一般的

// パターン2:__tests__フォルダにまとめる
src/
  components/
    Button.tsx
  __tests__/
    Button.test.tsx

どちらでも動きますが、テスト対象ファイルの隣に置くパターンの方が、どのコンポーネントにテストがあるかひと目でわかるので使いやすいです。

ファイル名の拡張子:

  • Button.test.tsx — 最も一般的
  • Button.spec.tsx — specとtestはどちらも使われる

よくある疑問 Q&A

Q. テストはいつ書くべきですか?

実装の前(TDD)、実装と同時、実装の後、どれでも構いません。フレッシャーのうちは実装の後に書くのが一番学びやすいです。まず動くものを作って、それからテストを書きながら「このコードはどう動くべきか」を整理する練習をしましょう。

Q. テストのカバレッジ(Coverage)は何%を目指すべきですか?

よく「80%以上」などと言われますが、数字自体にこだわりすぎる必要はありません。カバレッジ100%を達成しても、意味のないテストばかりでは意味がありません。重要なロジックとユーザーフローをカバーすることを優先してください。

Q. UIのスタイルもテストするべきですか?

基本的にはNOです。CSSのスタイルは変わりやすく、ピクセル単位のスタイルをテストしてもメンテナンスが大変なだけです。ただし「このボタンは disabled のとき見た目が変わる」のような機能的な状態変化はテストする価値があります。

Q. テストが遅くて待てない場合はどうすればいいですか?

Vitestはデフォルトでウォッチモードで動きます。変更したファイルに関連するテストだけが再実行されるので、実際にはとても速く感じます。

npm run test -- --watch  # 変更を監視して自動実行

Q. テストを書く時間がないときはどうすればいいですか?

現実的な話をすると、全部にテストを書く必要はありません。優先順位をつけて:

  1. バグが出やすい複雑なロジック
  2. 多くのページで使われる共通コンポーネント
  3. 重要なユーザーフロー(ログイン、決済など)

この順番でテストを書いていくのが効率的です。


まとめ

この記事では以下のことを学びました:

  • テストとは何か — コードが期待通りに動くことを自動確認する仕組み
  • 3種類のテスト — ユニット・インテグレーション・E2E、それぞれの特徴
  • なぜFEにもテストが必要か — リグレッション防止、リファクタリングの安心感、コード品質向上、ドキュメント化、採用市場での差別化
  • 使うツール — Vitest(テストランナー)+ React Testing Library(コンポーネントテスト)
  • 基本的な環境セットアップ — インストールと設定ファイル
  • テストコードの基本構文 — describe / it / expect とAAAパターン

次のPart 2では、実際にReactコンポーネントのテストを書いていきます。シンプルなコンポーネントから始めて、クリックイベントや条件付きレンダリングのテストまで、手を動かしながら学んでいきましょう。


参考リンク


いいねやコメントいただけると励みになります!


次回予告

Part 2 — Reactコンポーネントの実践テスト入門
実際にReactコンポーネントのテストを書いていきます。シンプルなコンポーネントから始めて、クリックイベントや条件付きレンダリングのテストまで、手を動かしながら学んでいきましょう。

5
5
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
5
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?