T-CREATOR

Vite と Jest を組み合わせたテスト環境の最適解

Vite と Jest を組み合わせたテスト環境の最適解

モダンフロントエンド開発において、Vite の高速な開発体験と Jest の豊富なテスト機能を組み合わせることで、効率的で信頼性の高い開発環境を構築できます。しかし、この組み合わせには特有の設定課題があり、多くの開発者が初期導入で躓いてしまいます。本記事では、実際のエラーケースを交えながら、Vite + Jest 環境の最適な構築方法をご紹介します。

背景

Vite の台頭とモダンフロントエンド開発

近年のフロントエンド開発において、Vite は圧倒的な速度でビルド環境のデファクトスタンダードとなりつつあります。ES モジュールのネイティブサポートと esbuild による高速な変換処理により、従来の Webpack ベースの環境と比較して劇的な開発体験の向上を実現しています。

特に以下の点で優れた特徴を持っています:

#特徴従来環境との比較
1開発サーバー起動時間数秒 → 数百ミリ秒
2ホットリロード速度秒単位 → ミリ秒単位
3バンドルサイズ最適化手動設定 → 自動最適化

Jest の位置づけとテスト環境の重要性

Jest は JavaScript エコシステムにおいて最も成熟したテスティングフレームワークの一つです。Facebook(現 Meta)によって開発され、ゼロコンフィグでの導入、豊富なマッチャー、モッキング機能、スナップショットテストなど、包括的なテスト機能を提供しています。

React、Vue、Angular といった主要フレームワークでの標準的な採用により、多くの開発者にとって馴染み深いツールとなっています。

課題

Vite と Jest の互換性問題

Vite と Jest を組み合わせる際に最も頻繁に遭遇するのが、ES モジュールの取り扱いに関する互換性問題です。

代表的なエラーケース 1:ES モジュール導入エラー

swiftSyntaxError: Cannot use import statement outside a module
    at wrapSafe (internal/modules/cjs/loader.js:915:16)
    at Module._compile (internal/modules/cjs/loader.js:963:27)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1027:10)

このエラーは、Jest がデフォルトで CommonJS モジュールシステムを使用するのに対し、Vite で作成されたコードが ES モジュールを前提としているために発生します。

代表的なエラーケース 2:型定義の競合

rusterror TS2322: Type 'typeof import("vite")' is not assignable to type 'typeof import("jest")'
  Property 'defineConfig' is missing in type 'typeof import("jest")' but required in type 'typeof import("vite")'

TypeScript 環境では、Vite と Jest の設定ファイルで異なる型定義が競合することがあります。

設定の複雑さと初期導入の壁

Vite + Jest 環境の構築では、以下のような複数の設定ファイルを適切に調整する必要があります:

#設定ファイル主な役割
1vite.config.tsVite ビルド設定
2jest.config.jsJest テスト設定
3tsconfig.jsonTypeScript 設定
4package.jsonパッケージ管理とスクリプト

これらの設定が相互に影響し合うため、一つの設定ミスが全体の動作に影響を与える可能性があります。

解決策

Vite + Jest 環境構築の最適解

この組み合わせを成功させるためには、段階的なアプローチと適切な設定パターンの選択が重要です。

基本方針

  1. ES モジュール対応の設定統一
  2. TypeScript 環境の適切な分離
  3. 依存関係の最小化と最適化

推奨設定パターンの紹介

最も安定した動作を実現する推奨パターンをご紹介します。このパターンでは、Vite と Jest それぞれの強みを活かしながら、互換性問題を最小限に抑えます。

パッケージ依存関係の最適化

まず、必要最小限の依存関係を明確に定義します:

json{
  "devDependencies": {
    "@types/jest": "^29.5.5",
    "@vitejs/plugin-react": "^4.1.0",
    "jest": "^29.7.0",
    "jest-environment-jsdom": "^29.7.0",
    "ts-jest": "^29.1.1",
    "typescript": "^5.2.2",
    "vite": "^4.4.9"
  }
}

この設定では、Jest 環境での React コンポーネントテストに必要な jest-environment-jsdom と、TypeScript 対応のための ts-jest を含めています。

具体例

実際の設定ファイル作成手順

ステップ 1:プロジェクト初期化

新規プロジェクトの場合、以下のコマンドで Vite プロジェクトを作成します:

bashyarn create vite my-vite-jest-project --template react-ts
cd my-vite-jest-project
yarn install

ステップ 2:Jest 関連パッケージの追加

次に、テスト環境に必要なパッケージを追加します:

bashyarn add -D jest @types/jest jest-environment-jsdom ts-jest

ステップ 3:Jest 設定ファイルの作成

プロジェクトルートに jest.config.js を作成し、Vite と Jest の橋渡しを行う設定を記述します:

javascript/** @type {import('jest').Config} */
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
  moduleNameMapping: {
    '^@/(.*)$': '<rootDir>/src/$1',
  },
  transform: {
    '^.+\\.tsx?$': 'ts-jest',
  },
};

この設定により、TypeScript ファイルの変換とパスエイリアスの解決が適切に行われます。

ステップ 4:TypeScript 設定の調整

Vite 用と Jest 用の TypeScript 設定を分離するため、tsconfig.json を以下のように設定します:

json{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  },
  "include": ["src"],
  "exclude": ["**/*.test.ts", "**/*.test.tsx"]
}

ステップ 5:テスト用 TypeScript 設定

テスト専用の設定として tsconfig.test.json を作成します:

json{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "module": "CommonJS",
    "types": ["jest", "node", "@testing-library/jest-dom"]
  },
  "include": [
    "src/**/*.test.ts",
    "src/**/*.test.tsx",
    "src/setupTests.ts"
  ]
}

この分離により、Vite と Jest それぞれに最適化された TypeScript 環境を提供できます。

TypeScript 対応の実装例

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

テスト環境の初期化のため、src​/​setupTests.ts を作成します:

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

// グローバルなテスト設定
Object.defineProperty(window, 'matchMedia', {
  writable: true,
  value: jest.fn().mockImplementation((query) => ({
    matches: false,
    media: query,
    onchange: null,
    addListener: jest.fn(),
    removeListener: jest.fn(),
    addEventListener: jest.fn(),
    removeEventListener: jest.fn(),
    dispatchEvent: jest.fn(),
  })),
});

サンプルコンポーネントとテスト

実際にテスト可能な React コンポーネントを作成します:

typescript// src/components/Counter.tsx
import { useState } from 'react';

interface CounterProps {
  initialValue?: number;
}

export const Counter = ({
  initialValue = 0,
}: CounterProps) => {
  const [count, setCount] = useState(initialValue);

  return (
    <div>
      <p data-testid='count-display'>Count: {count}</p>
      <button
        data-testid='increment-button'
        onClick={() => setCount(count + 1)}
      >
        Increment
      </button>
      <button
        data-testid='decrement-button'
        onClick={() => setCount(count - 1)}
      >
        Decrement
      </button>
    </div>
  );
};

対応するテストファイルを作成します:

typescript// src/components/Counter.test.tsx
import {
  render,
  screen,
  fireEvent,
} from '@testing-library/react';
import { Counter } from './Counter';

describe('Counter Component', () => {
  test('初期値が正しく表示される', () => {
    render(<Counter initialValue={5} />);
    expect(
      screen.getByTestId('count-display')
    ).toHaveTextContent('Count: 5');
  });

  test('インクリメントボタンが正しく動作する', () => {
    render(<Counter />);
    const incrementButton = screen.getByTestId(
      'increment-button'
    );

    fireEvent.click(incrementButton);
    expect(
      screen.getByTestId('count-display')
    ).toHaveTextContent('Count: 1');
  });
});

package.json スクリプトの設定

テスト実行のための npm スクリプトを package.json に追加します:

json{
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview",
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage"
  }
}

よくある設定エラーの解決

実際の導入過程で遭遇する可能性のあるエラーと解決方法をご紹介します:

エラー例 1:モジュール解決の失敗

arduinoCannot find module '@/components/Counter' from 'src/components/Counter.test.tsx'

このエラーは、Jest がパスエイリアスを正しく解決できない場合に発生します。解決には、jest.config.jsmoduleNameMapping 設定を確認してください。

エラー例 2:JSX の変換エラー

javascriptSyntaxError: Unexpected token '<'

JSX の変換設定が不適切な場合に発生します。ts-jest の設定で以下を追加します:

javascriptmodule.exports = {
  // 既存設定...
  globals: {
    'ts-jest': {
      tsconfig: {
        jsx: 'react-jsx',
      },
    },
  },
};

まとめ

Vite と Jest を組み合わせたテスト環境は、適切な設定により非常に強力な開発体験を提供します。重要なポイントは以下の通りです:

  1. ES モジュールと CommonJS の適切な使い分け:Vite と Jest それぞれに最適化された設定の分離
  2. TypeScript 設定の最適化:開発用とテスト用の設定ファイルの分離による競合回避
  3. 段階的な導入アプローチ:基本設定から始めて、必要に応じて高度な機能を追加

これらの設定により、高速な開発サーバーと信頼性の高いテスト環境を両立できます。特に大規模なプロジェクトにおいて、この組み合わせの恩恵を強く感じられるでしょう。

モダンフロントエンド開発において、テスト環境の充実は品質向上に直結します。今回ご紹介した設定パターンを参考に、ぜひプロジェクトに最適な Vite + Jest 環境を構築してみてください。

関連リンク