T-CREATOR

Jest と Testing Library の違いと使い分け

Jest と Testing Library の違いと使い分け

フロントエンド開発において、テストツールの選択は品質の高いアプリケーションを構築する上で重要な決断の一つです。特にJestTesting Libraryという二つの主要なツールについて、多くの開発者が「どちらを使うべきか」「併用できるのか」という疑問を抱いているのではないでしょうか。

この記事では、Jest と Testing Library の本質的な違いから実践的な使い分けまで、テストツール選択の決定版ガイドとして詳しく解説いたします。それぞれの特徴を理解し、プロジェクトに最適な選択ができるようになりましょう。

Jest と Testing Library の基本概念

Jest とは何か

Jest は、Meta(旧 Facebook)によって開発された JavaScript テスティングフレームワークです。テストランナーとして機能し、テストの実行環境を提供します。

Jest の主要機能

javascript// package.json
{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage"
  }
}

Jest は以下の機能をオールインワンで提供しています:

#機能説明
1テストランナーテストファイルの検出・実行
2アサーションexpect().toBe()などの検証機能
3モック機能外部依存関係の偽実装
4カバレッジ測定コードカバレッジの算出
5スナップショットUI の変更検知テスト

Jest 単体の強み

javascript// Jest単体でのシンプルなテスト例
describe('計算関数のテスト', () => {
  test('足し算が正しく動作する', () => {
    const add = (a, b) => a + b;
    expect(add(2, 3)).toBe(5);
  });

  test('非同期処理のテスト', async () => {
    const fetchData = () => Promise.resolve('データ');
    const result = await fetchData();
    expect(result).toBe('データ');
  });
});

Testing Library とは何か

Testing Library(主に React Testing Library)は、テストユーティリティライブラリです。「ユーザーがどのように使用するか」という観点からテストを記述するためのヘルパー関数を提供します。

Testing Library の設計思想

Testing Library は「実装の詳細ではなく、動作をテストする」という哲学に基づいています。

javascript// React Testing Libraryの基本例
import {
  render,
  screen,
  fireEvent,
} from '@testing-library/react';
import '@testing-library/jest-dom';

test('ボタンクリックでテキストが変更される', () => {
  const Button = () => {
    const [text, setText] = useState('初期値');
    return (
      <button onClick={() => setText('変更後')}>
        {text}
      </button>
    );
  };

  render(<Button />);

  // ユーザーの視点でテスト
  const button = screen.getByRole('button');
  expect(button).toHaveTextContent('初期値');

  fireEvent.click(button);
  expect(button).toHaveTextContent('変更後');
});

Testing Library が提供する機能

#機能説明
1レンダリングコンポーネントの仮想 DOM 生成
2クエリ関数DOM 要素の取得
3イベントシミュレーションユーザー操作の再現
4非同期処理待機状態変更の待機
5ユーザー中心 APIアクセシビリティを重視した要素取得

根本的な違いと役割分担

テストランナー vs テストユーティリティ

Jest と Testing Library の最も重要な違いは、それぞれが担う役割です。

Jest:テスト実行基盤

javascript// Jest設定ファイル例(jest.config.js)
module.exports = {
  testEnvironment: 'jsdom', // ブラウザ環境をシミュレート
  setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
  collectCoverage: true,
  coverageDirectory: 'coverage',
  testMatch: [
    '<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}',
    '<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}',
  ],
};

Testing Library:テスト記述支援

javascript// setupTests.js でTesting Libraryを設定
import '@testing-library/jest-dom';

// カスタムマッチャーが使用可能になる
expect(element).toBeInTheDocument();
expect(element).toHaveTextContent('テキスト');

提供する機能の範囲

両者の機能範囲を明確に理解することで、適切な使い分けができるようになります。

Jest 単体でできること

javascript// Jest単体での一般的なテスト
describe('APIクライアントのテスト', () => {
  // モック機能
  const mockFetch = jest.fn();
  global.fetch = mockFetch;

  beforeEach(() => {
    mockFetch.mockClear();
  });

  test('APIエラーハンドリング', async () => {
    // エラーレスポンスをモック
    mockFetch.mockRejectedValue(new Error('Network Error'));

    const apiClient = new ApiClient();

    await expect(apiClient.getData()).rejects.toThrow(
      'Network Error'
    );
  });
});

Testing Library 単体の制限

javascript// Testing Libraryにはテストランナー機能がない
// これだけでは実行できない
import { render, screen } from '@testing-library/react';

// describe, test, expect などはJestが提供
// Testing Libraryだけでは使用不可
render(<MyComponent />);
const element = screen.getByText('テキスト');
// expect関数もJest由来
expect(element).toBeInTheDocument();

実際のエラー例:Testing Library 単体使用時

Testing Library だけをインストールしてテストを実行すると、以下のエラーが発生します:

vbnetReferenceError: describe is not defined
    at Object.<anonymous> (src/App.test.js:5:1)
    at Module._compile (internal/modules/cjs/loader.js:1063:30)

TypeError: expect is not defined
    at Object.<anonymous> (src/App.test.js:12:5)

このエラーは、describeexpectが Jest によって提供される関数であり、Testing Library には含まれていないことを示しています。

具体的な機能比較

アサーション機能

Jest のアサーション

javascriptdescribe('Jestアサーションの例', () => {
  test('基本的なマッチャー', () => {
    // 数値比較
    expect(2 + 2).toBe(4);
    expect(2.1 + 2.2).toBeCloseTo(4.3, 1);

    // オブジェクト比較
    expect({ name: 'John' }).toEqual({ name: 'John' });

    // 配列・文字列
    expect(['apple', 'banana']).toContain('apple');
    expect('Hello World').toMatch(/World/);

    // 例外テスト
    expect(() => {
      throw new Error('エラー');
    }).toThrow('エラー');
  });
});

Testing Library のアサーション拡張

javascriptimport '@testing-library/jest-dom';

test('DOM要素専用のアサーション', () => {
  render(<input placeholder='名前を入力' disabled />);

  const input = screen.getByPlaceholderText('名前を入力');

  // Testing Libraryが拡張したマッチャー
  expect(input).toBeInTheDocument();
  expect(input).toBeDisabled();
  expect(input).toHaveAttribute(
    'placeholder',
    '名前を入力'
  );

  // Jestの標準マッチャーも使用可能
  expect(input.tagName).toBe('INPUT');
});

モック機能

Jest のモック機能

javascript// 関数モック
const mockCallback = jest.fn();
mockCallback('arg1', 'arg2');

expect(mockCallback).toHaveBeenCalledWith('arg1', 'arg2');
expect(mockCallback).toHaveBeenCalledTimes(1);

// モジュールモック
jest.mock('axios', () => ({
  get: jest.fn(() =>
    Promise.resolve({ data: 'テストデータ' })
  ),
}));

// タイマーモック
jest.useFakeTimers();
jest.advanceTimersByTime(1000);

Testing Library とモックの組み合わせ

javascript// API呼び出しコンポーネントのテスト
jest.mock('../api/userApi', () => ({
  fetchUser: jest.fn(),
}));

import { fetchUser } from '../api/userApi';

test('ユーザー情報を表示する', async () => {
  // Jestでモック設定
  fetchUser.mockResolvedValue({
    id: 1,
    name: '山田太郎',
  });

  // Testing Libraryでレンダリング
  render(<UserProfile userId={1} />);

  // 非同期処理を待機(Testing Library機能)
  await waitFor(() => {
    expect(
      screen.getByText('山田太郎')
    ).toBeInTheDocument();
  });

  // モック呼び出しを検証(Jest機能)
  expect(fetchUser).toHaveBeenCalledWith(1);
});

DOM 操作・クエリ機能

Jest での制限

Jest は基本的に DOM 操作機能を提供していません。DOM 要素の取得には標準の DOM API を使用する必要があります。

javascript// Jest単体でのDOM操作(制限的)
test('DOM操作の例', () => {
  document.body.innerHTML = `
    <div>
      <button id="test-button">クリック</button>
    </div>
  `;

  const button = document.getElementById('test-button');
  expect(button.textContent).toBe('クリック');

  // イベント発火も手動で実装
  const event = new Event('click');
  button.dispatchEvent(event);
});

Testing Library の強力なクエリ機能

javascripttest('Testing Libraryのクエリ機能', () => {
  render(
    <form>
      <label htmlFor='username'>ユーザー名</label>
      <input id='username' type='text' />
      <button type='submit'>送信</button>
    </form>
  );

  // 様々な取得方法
  const input = screen.getByLabelText('ユーザー名');
  const button = screen.getByRole('button', {
    name: '送信',
  });
  const form = screen.getByRole('form');

  // アクセシビリティを重視した取得
  expect(input).toBeInTheDocument();
  expect(button).toBeInTheDocument();
});

よくあるエラー:要素が見つからない場合

javascriptTestingLibraryElementError: Unable to find an element with the text: ログイン

Ignored nodes: comments, <script />, <style />
<body>
  <div>
    <button>
      Login
    </button>
  </div>
</body>

このエラーは、日本語の「ログイン」というテキストを探しているが、実際には英語の「Login」になっている場合に発生します。

実際のコード例で見る使い分け

Jest 単体でのテスト例

ビジネスロジックのテスト

javascript// src/utils/calculator.js
export class Calculator {
  add(a, b) {
    if (typeof a !== 'number' || typeof b !== 'number') {
      throw new Error('引数は数値である必要があります');
    }
    return a + b;
  }

  divide(a, b) {
    if (b === 0) {
      throw new Error('ゼロで割ることはできません');
    }
    return a / b;
  }
}

// src/utils/calculator.test.js
import { Calculator } from './calculator';

describe('Calculator', () => {
  let calculator;

  beforeEach(() => {
    calculator = new Calculator();
  });

  describe('add メソッド', () => {
    test('正常な加算処理', () => {
      expect(calculator.add(2, 3)).toBe(5);
      expect(calculator.add(-1, 1)).toBe(0);
      expect(calculator.add(0.1, 0.2)).toBeCloseTo(0.3);
    });

    test('無効な引数のエラーハンドリング', () => {
      expect(() => calculator.add('2', 3)).toThrow(
        '引数は数値である必要があります'
      );
      expect(() => calculator.add(null, 3)).toThrow();
    });
  });

  describe('divide メソッド', () => {
    test('正常な除算処理', () => {
      expect(calculator.divide(10, 2)).toBe(5);
      expect(calculator.divide(7, 3)).toBeCloseTo(2.333, 3);
    });

    test('ゼロ除算エラー', () => {
      expect(() => calculator.divide(10, 0)).toThrow(
        'ゼロで割ることはできません'
      );
    });
  });
});

API クライアントのテスト

javascript// src/api/userApi.js
export class UserApi {
  constructor(httpClient) {
    this.httpClient = httpClient;
  }

  async getUser(id) {
    try {
      const response = await this.httpClient.get(
        `/users/${id}`
      );
      return response.data;
    } catch (error) {
      if (error.response?.status === 404) {
        throw new Error(
          `ユーザーID ${id} が見つかりません`
        );
      }
      throw new Error('ユーザー情報の取得に失敗しました');
    }
  }
}

// src/api/userApi.test.js
import { UserApi } from './userApi';

describe('UserApi', () => {
  let userApi;
  let mockHttpClient;

  beforeEach(() => {
    mockHttpClient = {
      get: jest.fn(),
    };
    userApi = new UserApi(mockHttpClient);
  });

  test('ユーザー情報を正常に取得', async () => {
    const userData = { id: 1, name: '山田太郎' };
    mockHttpClient.get.mockResolvedValue({
      data: userData,
    });

    const result = await userApi.getUser(1);

    expect(result).toEqual(userData);
    expect(mockHttpClient.get).toHaveBeenCalledWith(
      '/users/1'
    );
  });

  test('404エラーの適切な処理', async () => {
    const error = {
      response: { status: 404 },
    };
    mockHttpClient.get.mockRejectedValue(error);

    await expect(userApi.getUser(999)).rejects.toThrow(
      'ユーザーID 999 が見つかりません'
    );
  });

  test('その他のエラーの処理', async () => {
    mockHttpClient.get.mockRejectedValue(
      new Error('Network Error')
    );

    await expect(userApi.getUser(1)).rejects.toThrow(
      'ユーザー情報の取得に失敗しました'
    );
  });
});

Jest + Testing Library でのテスト例

React コンポーネントのテスト

javascript// src/components/UserProfile.jsx
import React, { useState, useEffect } from 'react';

export const UserProfile = ({ userId, userApi }) => {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchUser = async () => {
      setLoading(true);
      setError(null);

      try {
        const userData = await userApi.getUser(userId);
        setUser(userData);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    if (userId) {
      fetchUser();
    }
  }, [userId, userApi]);

  if (loading) return <div>読み込み中...</div>;
  if (error) return <div role='alert'>エラー: {error}</div>;
  if (!user) return <div>ユーザーが選択されていません</div>;

  return (
    <div>
      <h2>{user.name}</h2>
      <p>メール: {user.email}</p>
      <p>年齢: {user.age}歳</p>
    </div>
  );
};

// src/components/UserProfile.test.jsx
import React from 'react';
import {
  render,
  screen,
  waitFor,
} from '@testing-library/react';
import '@testing-library/jest-dom';
import { UserProfile } from './UserProfile';

describe('UserProfile コンポーネント', () => {
  let mockUserApi;

  beforeEach(() => {
    mockUserApi = {
      getUser: jest.fn(),
    };
  });

  test('ユーザー情報を正常に表示', async () => {
    const userData = {
      name: '山田太郎',
      email: 'yamada@example.com',
      age: 30,
    };

    // Jest: APIをモック
    mockUserApi.getUser.mockResolvedValue(userData);

    // Testing Library: コンポーネントをレンダリング
    render(
      <UserProfile userId={1} userApi={mockUserApi} />
    );

    // Testing Library: ローディング状態を確認
    expect(
      screen.getByText('読み込み中...')
    ).toBeInTheDocument();

    // Testing Library: 非同期処理を待機
    await waitFor(() => {
      expect(
        screen.getByText('山田太郎')
      ).toBeInTheDocument();
    });

    // Testing Library: 表示内容を検証
    expect(
      screen.getByText('メール: yamada@example.com')
    ).toBeInTheDocument();
    expect(
      screen.getByText('年齢: 30歳')
    ).toBeInTheDocument();

    // Jest: API呼び出しを検証
    expect(mockUserApi.getUser).toHaveBeenCalledWith(1);
    expect(mockUserApi.getUser).toHaveBeenCalledTimes(1);
  });

  test('エラー状態の表示', async () => {
    const errorMessage = 'ユーザーID 999 が見つかりません';

    // Jest: エラーをモック
    mockUserApi.getUser.mockRejectedValue(
      new Error(errorMessage)
    );

    render(
      <UserProfile userId={999} userApi={mockUserApi} />
    );

    // Testing Library: エラー表示を確認
    await waitFor(() => {
      const errorElement = screen.getByRole('alert');
      expect(errorElement).toHaveTextContent(
        `エラー: ${errorMessage}`
      );
    });
  });

  test('ユーザーIDが未指定の場合', () => {
    render(
      <UserProfile userId={null} userApi={mockUserApi} />
    );

    expect(
      screen.getByText('ユーザーが選択されていません')
    ).toBeInTheDocument();
    expect(mockUserApi.getUser).not.toHaveBeenCalled();
  });
});

フォームコンポーネントのテスト

javascript// src/components/ContactForm.jsx
import React, { useState } from 'react';

export const ContactForm = ({ onSubmit }) => {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    message: '',
  });
  const [errors, setErrors] = useState({});
  const [isSubmitting, setIsSubmitting] = useState(false);

  const validateForm = () => {
    const newErrors = {};

    if (!formData.name.trim()) {
      newErrors.name = '名前は必須です';
    }

    if (!formData.email.trim()) {
      newErrors.email = 'メールアドレスは必須です';
    } else if (!/\S+@\S+\.\S+/.test(formData.email)) {
      newErrors.email =
        '有効なメールアドレスを入力してください';
    }

    if (!formData.message.trim()) {
      newErrors.message = 'メッセージは必須です';
    }

    return newErrors;
  };

  const handleSubmit = async (e) => {
    e.preventDefault();

    const validationErrors = validateForm();
    if (Object.keys(validationErrors).length > 0) {
      setErrors(validationErrors);
      return;
    }

    setIsSubmitting(true);
    setErrors({});

    try {
      await onSubmit(formData);
      setFormData({ name: '', email: '', message: '' });
    } catch (error) {
      setErrors({ submit: '送信に失敗しました' });
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <form onSubmit={handleSubmit} noValidate>
      <div>
        <label htmlFor='name'>名前 *</label>
        <input
          id='name'
          type='text'
          value={formData.name}
          onChange={(e) =>
            setFormData({
              ...formData,
              name: e.target.value,
            })
          }
          aria-invalid={errors.name ? 'true' : 'false'}
          aria-describedby={
            errors.name ? 'name-error' : undefined
          }
        />
        {errors.name && (
          <div id='name-error' role='alert'>
            {errors.name}
          </div>
        )}
      </div>

      <div>
        <label htmlFor='email'>メールアドレス *</label>
        <input
          id='email'
          type='email'
          value={formData.email}
          onChange={(e) =>
            setFormData({
              ...formData,
              email: e.target.value,
            })
          }
          aria-invalid={errors.email ? 'true' : 'false'}
          aria-describedby={
            errors.email ? 'email-error' : undefined
          }
        />
        {errors.email && (
          <div id='email-error' role='alert'>
            {errors.email}
          </div>
        )}
      </div>

      <div>
        <label htmlFor='message'>メッセージ *</label>
        <textarea
          id='message'
          value={formData.message}
          onChange={(e) =>
            setFormData({
              ...formData,
              message: e.target.value,
            })
          }
          aria-invalid={errors.message ? 'true' : 'false'}
          aria-describedby={
            errors.message ? 'message-error' : undefined
          }
        />
        {errors.message && (
          <div id='message-error' role='alert'>
            {errors.message}
          </div>
        )}
      </div>

      {errors.submit && (
        <div role='alert'>{errors.submit}</div>
      )}

      <button type='submit' disabled={isSubmitting}>
        {isSubmitting ? '送信中...' : '送信'}
      </button>
    </form>
  );
};

// src/components/ContactForm.test.jsx
import React from 'react';
import {
  render,
  screen,
  fireEvent,
  waitFor,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import '@testing-library/jest-dom';
import { ContactForm } from './ContactForm';

describe('ContactForm コンポーネント', () => {
  let mockOnSubmit;
  let user;

  beforeEach(() => {
    mockOnSubmit = jest.fn();
    user = userEvent.setup();
  });

  test('フォームが正常にレンダリングされる', () => {
    render(<ContactForm onSubmit={mockOnSubmit} />);

    expect(
      screen.getByLabelText('名前 *')
    ).toBeInTheDocument();
    expect(
      screen.getByLabelText('メールアドレス *')
    ).toBeInTheDocument();
    expect(
      screen.getByLabelText('メッセージ *')
    ).toBeInTheDocument();
    expect(
      screen.getByRole('button', { name: '送信' })
    ).toBeInTheDocument();
  });

  test('有効なデータでフォーム送信が成功する', async () => {
    // Jest: onSubmitをモック
    mockOnSubmit.mockResolvedValue();

    render(<ContactForm onSubmit={mockOnSubmit} />);

    // Testing Library: フォーム入力
    await user.type(
      screen.getByLabelText('名前 *'),
      '山田太郎'
    );
    await user.type(
      screen.getByLabelText('メールアドレス *'),
      'yamada@example.com'
    );
    await user.type(
      screen.getByLabelText('メッセージ *'),
      'お問い合わせです'
    );

    const submitButton = screen.getByRole('button', {
      name: '送信',
    });
    await user.click(submitButton);

    // Jest: コールバック関数の呼び出しを検証
    await waitFor(() => {
      expect(mockOnSubmit).toHaveBeenCalledWith({
        name: '山田太郎',
        email: 'yamada@example.com',
        message: 'お問い合わせです',
      });
    });

    // Testing Library: フォームリセットを確認
    expect(screen.getByLabelText('名前 *')).toHaveValue('');
    expect(
      screen.getByLabelText('メールアドレス *')
    ).toHaveValue('');
    expect(
      screen.getByLabelText('メッセージ *')
    ).toHaveValue('');
  });

  test('バリデーションエラーの表示', async () => {
    render(<ContactForm onSubmit={mockOnSubmit} />);

    // 空の状態で送信
    const submitButton = screen.getByRole('button', {
      name: '送信',
    });
    await user.click(submitButton);

    // Testing Library: エラーメッセージの表示を確認
    await waitFor(() => {
      expect(
        screen.getByText('名前は必須です')
      ).toBeInTheDocument();
      expect(
        screen.getByText('メールアドレスは必須です')
      ).toBeInTheDocument();
      expect(
        screen.getByText('メッセージは必須です')
      ).toBeInTheDocument();
    });

    // Jest: onSubmitが呼ばれていないことを確認
    expect(mockOnSubmit).not.toHaveBeenCalled();
  });

  test('無効なメールアドレスのバリデーション', async () => {
    render(<ContactForm onSubmit={mockOnSubmit} />);

    await user.type(
      screen.getByLabelText('メールアドレス *'),
      '無効なメール'
    );

    const submitButton = screen.getByRole('button', {
      name: '送信',
    });
    await user.click(submitButton);

    await waitFor(() => {
      expect(
        screen.getByText(
          '有効なメールアドレスを入力してください'
        )
      ).toBeInTheDocument();
    });
  });

  test('送信エラーの処理', async () => {
    // Jest: エラーをモック
    mockOnSubmit.mockRejectedValue(new Error('送信失敗'));

    render(<ContactForm onSubmit={mockOnSubmit} />);

    // 有効なデータを入力
    await user.type(
      screen.getByLabelText('名前 *'),
      '山田太郎'
    );
    await user.type(
      screen.getByLabelText('メールアドレス *'),
      'yamada@example.com'
    );
    await user.type(
      screen.getByLabelText('メッセージ *'),
      'テスト'
    );

    const submitButton = screen.getByRole('button', {
      name: '送信',
    });
    await user.click(submitButton);

    // Testing Library: エラーメッセージの表示を確認
    await waitFor(() => {
      expect(
        screen.getByText('送信に失敗しました')
      ).toBeInTheDocument();
    });

    // 送信ボタンが元に戻ることを確認
    expect(
      screen.getByRole('button', { name: '送信' })
    ).toBeInTheDocument();
  });

  test('送信中状態の表示', async () => {
    // Jest: 送信処理を遅延させる
    mockOnSubmit.mockImplementation(
      () =>
        new Promise((resolve) => setTimeout(resolve, 100))
    );

    render(<ContactForm onSubmit={mockOnSubmit} />);

    // フォーム入力
    await user.type(
      screen.getByLabelText('名前 *'),
      '山田太郎'
    );
    await user.type(
      screen.getByLabelText('メールアドレス *'),
      'yamada@example.com'
    );
    await user.type(
      screen.getByLabelText('メッセージ *'),
      'テスト'
    );

    const submitButton = screen.getByRole('button', {
      name: '送信',
    });
    await user.click(submitButton);

    // Testing Library: 送信中状態を確認
    expect(
      screen.getByRole('button', { name: '送信中...' })
    ).toBeDisabled();

    // 送信完了を待機
    await waitFor(() => {
      expect(
        screen.getByRole('button', { name: '送信' })
      ).not.toBeDisabled();
    });
  });
});

どちらを選ぶべきか:判断基準

プロジェクトの性質による使い分け

ライブラリ・ユーティリティ関数の開発

javascript// パッケージ開発時:Jest単体が最適
// src/utils/dateUtils.js
export const formatDate = (date, format = 'YYYY-MM-DD') => {
  // 日付フォーマット処理
};

export const addDays = (date, days) => {
  // 日付加算処理
};

// Jest単体でのテスト
describe('dateUtils', () => {
  test('formatDate - デフォルトフォーマット', () => {
    const date = new Date('2024-01-15');
    expect(formatDate(date)).toBe('2024-01-15');
  });

  test('addDays - 正常な日付加算', () => {
    const date = new Date('2024-01-15');
    const result = addDays(date, 7);
    expect(result.getDate()).toBe(22);
  });
});

React アプリケーション開発

javascript// React開発時:Jest + Testing Library の組み合わせが最適
test('検索フォームの動作', async () => {
  const mockOnSearch = jest.fn(); // Jest のモック機能

  render(<SearchForm onSearch={mockOnSearch} />); // Testing Library

  const input =
    screen.getByPlaceholderText('検索キーワード'); // Testing Library
  const button = screen.getByRole('button', {
    name: '検索',
  }); // Testing Library

  await userEvent.type(input, 'React'); // Testing Library
  await userEvent.click(button); // Testing Library

  expect(mockOnSearch).toHaveBeenCalledWith('React'); // Jest のアサーション
});

チーム構成による選択

経験豊富な開発チーム

javascript// 複雑なテストシナリオに対応可能
describe('高度なテストシナリオ', () => {
  test('複数のモックとタイマーを組み合わせ', async () => {
    // Jest の高度な機能を活用
    jest.useFakeTimers();

    const mockApi = jest.fn().mockImplementation(
      () =>
        new Promise((resolve) => {
          setTimeout(() => resolve({ data: 'test' }), 1000);
        })
    );

    render(<Component api={mockApi} />);

    // タイマーを進める
    jest.advanceTimersByTime(1000);

    await waitFor(() => {
      expect(screen.getByText('test')).toBeInTheDocument();
    });

    jest.useRealTimers();
  });
});

学習コストを重視するチーム

javascript// シンプルなテストから始める
test('基本的な表示テスト', () => {
  render(<WelcomeMessage name='太郎' />);

  // 直感的でわかりやすいテスト
  expect(
    screen.getByText('こんにちは、太郎さん!')
  ).toBeInTheDocument();
});

test('ボタンクリックテスト', async () => {
  const user = userEvent.setup();
  render(<Counter />);

  const button = screen.getByText('カウントアップ');
  await user.click(button);

  expect(
    screen.getByText('カウント: 1')
  ).toBeInTheDocument();
});

段階的導入戦略

フェーズ 1:Jest 単体で基礎固め

javascript// 最初はビジネスロジックのテストから
describe('商品価格計算', () => {
  test('消費税込み価格計算', () => {
    const calculateTaxIncluded = (price, taxRate = 0.1) => {
      return Math.floor(price * (1 + taxRate));
    };

    expect(calculateTaxIncluded(1000)).toBe(1100);
    expect(calculateTaxIncluded(999)).toBe(1098);
  });
});

フェーズ 2:Testing Library 追加

javascript// UIコンポーネントテストの導入
test('価格表示コンポーネント', () => {
  render(<PriceDisplay price={1000} taxRate={0.1} />);

  expect(
    screen.getByText('本体価格: ¥1,000')
  ).toBeInTheDocument();
  expect(
    screen.getByText('税込価格: ¥1,100')
  ).toBeInTheDocument();
});

フェーズ 3:統合テストの追加

javascript// 複雑なユーザーフローのテスト
test('商品購入フロー', async () => {
  const user = userEvent.setup();

  render(<ShoppingCart />);

  // 商品追加
  await user.click(screen.getByText('カートに追加'));
  expect(
    screen.getByText('カート (1)')
  ).toBeInTheDocument();

  // 決済へ進む
  await user.click(screen.getByText('レジに進む'));
  expect(
    screen.getByText('お支払い方法を選択')
  ).toBeInTheDocument();
});

よくある落とし穴と対策

設定不備によるエラー

javascriptError: Cannot find module '@testing-library/jest-dom'
    at Object.<anonymous> (src/setupTests.js:1:1)

SyntaxError: Cannot use import statement outside a module
    at Object.<anonymous> (src/components/App.test.jsx:1:1)

解決策:適切な設定

javascript// jest.config.js
module.exports = {
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
  transform: {
    '^.+\\.(js|jsx|ts|tsx)$': 'babel-jest',
  },
  moduleNameMapping: {
    '\\.(css|less|scss|sass)$': 'identity-obj-proxy',
  },
};

// babel.config.js
module.exports = {
  presets: [
    ['@babel/preset-env', { targets: { node: 'current' } }],
    ['@babel/preset-react', { runtime: 'automatic' }],
  ],
};

Testing Library 使用時の典型的エラー

javascriptTestingLibraryElementError: Unable to find an accessible element with the role "button" and name "送信"

Found the following similar elements:
<button type="submit">Submit</button>

解決策:適切なクエリの使用

javascript// 問題のあるテスト
const button = screen.getByRole('button', { name: '送信' });

// 修正版
const button = screen.getByRole('button', {
  name: 'Submit',
});
// または
const button = screen.getByRole('button', {
  name: /submit/i,
});

まとめ

Jest と Testing Library は、それぞれ異なる役割を持つ補完的なツールです。Jest はテスト実行基盤として、Testing Library はテスト記述支援として機能し、多くの場合、両者を組み合わせて使用することが最適解となります。

適切な選択指針

プロジェクトタイプ推奨構成理由
ライブラリ・ユーティリティ開発Jest 単体DOM 操作が不要、軽量構成
React/Vue アプリケーションJest + Testing LibraryUI テストに最適化
Node.js API 開発Jest 単体サーバーサイドロジックに集中
フルスタック開発Jest + Testing Libraryフロントエンド・バックエンド両対応

現代のフロントエンド開発において、テストは品質保証の要です。Jest と Testing Library それぞれの特徴を理解し、プロジェクトの要件に応じて適切に選択・組み合わせることで、保守性が高く信頼できるアプリケーションを構築できるでしょう。

まずは小さなプロジェクトから始めて、段階的にテストカバレッジを向上させていくことをお勧めします。テストが開発の重要なパートナーとなることで、より安心してコードを書けるようになるはずです。

関連リンク