•11 min read

VitestとReactTestingLibraryによるモダンなReactテスト

VitestとReactTestingLibraryによるモダンなReactテスト

Reactアプリケーションのテストは、長年にわたり大きく進化してきました。長い間、JestはJavaScriptテストエコシステムにおいて議論の余地のない王者でした。しかし、業界がESモジュール(ESM)やViteのような最新のビルドツールに移行するにつれて、Jestは特に実行速度とESM互換性に関して、その古さが目立つようになりました。そこに登場したのがVitestです。これは、Viteネイティブの超高速テストフレームワークであり、Reactプロジェクトの新しい標準となりつつあります。

この包括的なガイドでは、JestからVitestへの移行、React Testing Library(RTL)との統合、そして従来のチュートリアルを超える最新のテストパターンを活用する方法について深く掘り下げていきます。

Audio Briefing
0:00 / 0:00

JestからVitestへ移行する理由

コードに飛び込む前に、なぜこの変化が起きているのかを理解することが重要です。Robin Wieruchのような包括的なチュートリアルが歴史的にJestを支持してきた一方で、現代のフロントエンドの状況は、より速いイテレーションサイクルを求めています。

  1. ネイティブESMサポート: Jestは歴史的にESMとの連携に苦労しており、複雑なBabel設定や実験的なフラグが必要となることがよくありました。VitestはESMをすぐに利用できます。
  2. Vite統合: ReactアプリがすでにViteを使用している場合、Vitestは同じ設定(vite.config.ts)、プラグイン、変換パイプラインを共有します。これにより、「開発環境では動作するが、テストでは失敗する」という恐ろしい不一致が解消されます。
  3. 速度: ESBuildを変換に利用することで、VitestはJestよりもはるかに高速にテストを実行します。特に大規模なコードベースでは顕著です。
  4. ウォッチモード: Vitestのインテリジェントなウォッチモード(ViteのHMRメカニズムによって強化されています)は、ファイル変更の影響を受けるテストのみを即座に再実行します。
  5. API互換性: VitestはJest互換のAPIを提供しており、移行は驚くほど簡単です。describe、it、expect、およびvi(jestに相当するVitest)は、ほとんどの場合、そのまま置き換え可能です。
Advertisement

ステップ1:インストールと設定

まず、Jestを削除し、VitestとReact Testing Libraryをインストールすることから始めましょう。

npm uninstall jest @types/jest ts-jest @testing-library/jest-dom
npm install -D vitest @vitest/ui @testing-library/react @testing-library/jest-dom jsdom @testing-library/user-event

次に、Vitestを設定します。Viteを使用している場合は、vite.config.tsにtestプロパティを追加するだけです。

// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';

export default defineConfig({
  plugins: [react()],
  test: {
    globals: true, // Enables 'describe', 'it', 'expect' globally like Jest
    environment: 'jsdom', // Simulates a browser environment for React components
    setupFiles: './src/setupTests.ts', // Path to your setup file
    css: true, // Process CSS imports
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
});

環境のセットアップ

src/setupTests.tsを作成して、Vitestのexpectを@testing-library/jest-domからのDOMマッチャーで拡張します。

// src/setupTests.ts
import '@testing-library/jest-dom/vitest';
import { cleanup } from '@testing-library/react';
import { afterEach, vi } from 'vitest';

// Automatically unmount and cleanup DOM after the test is finished.
afterEach(() => {
  cleanup();
});

// Mocking window.matchMedia if needed for UI libraries
Object.defineProperty(window, 'matchMedia', {
  writable: true,
  value: vi.fn().mockImplementation(query => ({
    matches: false,
    media: query,
    onchange: null,
    addListener: vi.fn(),
    removeListener: vi.fn(),
    addEventListener: vi.fn(),
    removeEventListener: vi.fn(),
    dispatchEvent: vi.fn(),
  })),
});

@testing-library/jest-dom/vitestをインポートしていることに注目してください。これは、ネイティブのVitest型を提供し、Jestとの名前空間の衝突を避けるための新しい追加機能です。

ステップ2:移行プロセス(JestからVitestへ)

テストファイルの移行は、通常、検索と置換の操作です。

  1. グローバル: 設定でglobals: trueを設定している場合、vitestからdescribe、it、またはexpectをインポートする必要はありません。
  2. モック: jest.fn()をvi.fn()に、jest.spyOn()をvi.spyOn()に、jest.mock()をvi.mock()に置き換えます。
  3. タイマー: jest.useFakeTimers()をvi.useFakeTimers()に置き換えます。

高度なモックの例

モジュールのモックがどのように変わるかを見てみましょう。

Jest:

jest.mock('axios', () => ({
  default: { get: jest.fn().mockResolvedValue({ data: {} }) }
}));

Vitest:

import { vi } from 'vitest';
import axios from 'axios';

vi.mock('axios', () => ({
  default: { get: vi.fn().mockResolvedValue({ data: {} }) }
}));

構造は同じですが、Vitestの巻き上げ(hoisting)メカニズムはわずかに異なります。VitestはJestと同様にvi.mockをファイルの先頭に巻き上げますが、ESMとの統合がよりシームレスであるため、モックが適用される前にモックされたモジュールが評価されるというエッジケースが減少します。

ステップ3:React Testing Libraryによる最新のコンポーネントテスト

React Testing Library(RTL)は、実装の詳細をテストするのではなく、ユーザーの視点からコンポーネントをテストすることを推奨しています。複雑な非同期コンポーネントを構築し、適切にテストしてみましょう。

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

interface User {
  id: string;
  name: string;
  email: string;
}

export const UserProfile = ({ userId }: { userId: string }) => {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const fetchUser = async () => {
      try {
        setLoading(true);
        const response = await fetch(`/api/users/${userId}`);
        if (!response.ok) throw new Error('Failed to fetch user');
        const data = await response.json();
        setUser(data);
      } catch (err) {
        setError(err instanceof Error ? err.message : 'Unknown error');
      } finally {
        setLoading(false);
      }
    };
    fetchUser();
  }, [userId]);

  if (loading) return <div role="status">Loading...</div>;
  if (error) return <div role="alert">{error}</div>;
  if (!user) return <div>No user found</div>;

  return (
    <article>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
      <button onClick={() => console.log('Edit clicked')}>Edit Profile</button>
    </article>
  );
};

テストの記述

以下は、VitestとRTLを使用して、このコンポーネントの最新で堅牢なテストスイートを作成する方法です。waitForを不必要に使用するなどの一般的なアンチパターンを避けています。

// src/components/UserProfile.test.tsx
import { render, screen, waitForElementToBeRemoved } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { UserProfile } from './UserProfile';
import { vi, describe, it, expect, beforeEach } from 'vitest';

// Mock the global fetch
global.fetch = vi.fn();

describe('UserProfile Component', () => {
  const mockUser = { id: '1', name: 'Jane Doe', email: 'jane@example.com' };

  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('renders loading state initially, then displays user data', async () => {
    (global.fetch as any).mockResolvedValueOnce({
      ok: true,
      json: async () => mockUser,
    });

    render(<UserProfile userId="1" />);

    // 1. Assert loading state
    expect(screen.getByRole('status')).toHaveTextContent('Loading...');

    // 2. Wait for loading to finish (Modern pattern instead of waitFor)
    await waitForElementToBeRemoved(() => screen.queryByRole('status'));

    // 3. Assert user data
    expect(screen.getByRole('heading', { name: 'Jane Doe' })).toBeInTheDocument();
    expect(screen.getByText('jane@example.com')).toBeInTheDocument();
  });

  it('renders error state on API failure', async () => {
    (global.fetch as any).mockResolvedValueOnce({
      ok: false,
    });

    render(<UserProfile userId="2" />);

    // Wait for the alert role to appear
    const alertElement = await screen.findByRole('alert');
    expect(alertElement).toHaveTextContent('Failed to fetch user');
  });

  it('simulates user interaction correctly', async () => {
    (global.fetch as any).mockResolvedValueOnce({
      ok: true,
      json: async () => mockUser,
    });
    const consoleSpy = vi.spyOn(console, 'log');

    render(<UserProfile userId="1" />);
    
    // Wait for data to load using findByRole
    const editButton = await screen.findByRole('button', { name: /edit profile/i });

    // Use userEvent instead of fireEvent for more realistic interactions
    const user = userEvent.setup();
    await user.click(editButton);

    expect(consoleSpy).toHaveBeenCalledWith('Edit clicked');
  });
});

テストスイートからの重要なポイント:

  1. userEventよりもfireEvent: @testing-library/user-eventを利用しています。これは、完全なDOMイベント(キーボードフォーカス、バブリングなど)をシミュレートし、テストを実際のユーザーがブラウザとどのようにやり取りするかに大幅に近づけます。
  2. findBy*の代わりにwaitFor: getBy*をwaitForブロックで囲む代わりに、最新のRTLはawait screen.findBy*()を使用することを推奨しています。これはよりクリーンで、ポーリングを内部で処理します。
  3. waitForElementToBeRemoved: これは、ローディングスケルトンやスピナーが消えたことをアサートし、その後レンダリングされたコンテンツをアサートする最もクリーンな方法です。
Advertisement

継続的インテグレーションとカバレッジ

Vitestは、コードカバレッジのためにc8(v8経由)またはistanbulを使用します。カバレッジを有効にするには、以下を実行します。

npx vitest run --coverage

プロバイダーをインストールする必要があるかもしれません:npm i -D @vitest/coverage-v8。出力は即座に生成され、ESBuild上で実行されるため、CIパイプラインで驚くほど高速です。

結論

JestからVitestへの移行は、単にトレンドに追いつくことだけではありません。それは開発者体験のための実用的なアップグレードです。ネイティブESMサポート、Viteとの共有設定、そしてCI時間を半分に短縮できる実行速度が得られます。

React Testing Libraryの哲学的アプローチ(実装の詳細ではなく振る舞いをテストする)と組み合わせることで、テストスイートは壊れやすいメンテナンスの負担ではなく、堅牢なセーフティネットになります。Vitestを採用し、userEventを活用し、コンポーネントの内部状態をテストするのをやめましょう。ハッピーテスティング!

こちらもおすすめ

Share this article:

Stay Updated

Get the latest posts delivered straight to your inbox.

Free Developer Utilities

Free In-Browser Developer Tools

Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.

Explore Tools
Advertisement