← ブログ一覧

Vitest への移行ガイド:Jest からの段階的移行と実装パターン

Jest から Vitest へ移行する手順を実務視点で解説。設定ファイル・テストコード・カバレッジ・CI 統合まで、既存プロジェクトで即使える段階的移行ガイド。

#テスト#TypeScript#JavaScript#技術解説
Vitest への移行ガイド:Jest からの段階的移行と実装パターン

Vitest への移行ガイド:Jest からの段階的移行と実装パターン

Jest は長らく JavaScript / TypeScript のテストツールとして標準的な地位を占めてきましたが、ビルドツールの進化に伴い、Vite エコシステムと統合された Vitest が急速に普及しています。

本記事では、既存の Jest プロジェクトを Vitest へ段階的に移行する手順と、実務で遭遇する典型的な課題への対処方法を解説します。


1. Vitest を選ぶ理由と移行判断基準

Vitest の主要な利点

| 項目 | Jest | Vitest | |------|------|--------| | 起動速度 | 遅い(数秒〜数十秒) | 高速(1秒以下) | | ビルドツール統合 | 別途設定が必要 | Vite 設定を再利用 | | ESM サポート | 実験的・制約あり | ネイティブ対応 | | HMR(ホットリロード) | なし | あり(watch モード) | | 並列実行 | ワーカープロセス | スレッド(より軽量) | | 設定ファイル | jest.config.js | vite.config.ts で統合可能 |

移行を検討すべきプロジェクト

  • Vite / Vite ベースのフレームワーク(Next.js App Router, SvelteKit など)を使用
  • テスト実行速度に課題を感じている(CI で 5 分以上かかる)
  • ESM パッケージを多用(Jest の設定が煩雑)
  • 開発体験を重視(watch モードでの高速フィードバック)

移行を急がなくてよいケース

  • Jest の独自機能に強く依存(jest.mock のホイスティング挙動など)
  • 既存テストが数千件あり、移行コストが見合わない
  • React Native など Vite 非対応環境

2. 移行前の準備:互換性チェック

2-1. 依存パッケージの確認

# Jest 関連パッケージを列挙
npm list --depth=0 | grep jest

# 以下が一般的な構成
# jest
# @types/jest
# ts-jest (TypeScript の場合)
# @testing-library/jest-dom
# jest-environment-jsdom

2-2. テストファイルのパターン調査

# テストファイルの命名規則を確認
find . -name '*.test.*' -o -name '*.spec.*' | head -n 10

# Jest の設定ファイルを確認
cat jest.config.js

2-3. モック利用状況の確認

// Jest 固有のモック API を使っている箇所を検索
// jest.mock() のホイスティング挙動に依存していないか確認
grep -r "jest.mock" src/
grep -r "jest.spyOn" src/

重要な違い:Vitest の vi.mock()Jest ほど自動ホイスティングされない ため、import 文の順序に注意が必要です。


3. Vitest のインストールと基本設定

3-1. パッケージのインストール

# Vitest 本体と型定義
npm install -D vitest @vitest/ui

# DOM テスト用(React / Vue などの場合)
npm install -D jsdom @testing-library/react @testing-library/user-event

# カバレッジ計測用
npm install -D @vitest/coverage-v8

3-2. vite.config.ts に統合

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  test: {
    // グローバル API を有効化(describe, it, expect など)
    globals: true,
    // DOM 環境が必要な場合
    environment: 'jsdom',
    // セットアップファイル
    setupFiles: ['./src/test/setup.ts'],
    // カバレッジ設定
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      exclude: [
        'node_modules/',
        'src/test/',
        '**/*.test.{ts,tsx}',
        '**/*.spec.{ts,tsx}',
      ],
    },
    // タイムアウト設定
    testTimeout: 10000,
  },
});

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

// src/test/setup.ts
import { expect, afterEach } from 'vitest';
import { cleanup } from '@testing-library/react';
import * as matchers from '@testing-library/jest-dom/matchers';

// jest-dom のマッチャーを追加
expect.extend(matchers);

// 各テスト後に DOM をクリーンアップ
afterEach(() => {
  cleanup();
});

3-4. TypeScript 型定義の追加

// tsconfig.json
{
  "compilerOptions": {
    "types": ["vitest/globals", "@testing-library/jest-dom"]
  }
}

4. テストコードの段階的移行

4-1. 基本的な移行パターン

| Jest | Vitest | 備考 | |------|--------|------| | jest.fn() | vi.fn() | モック関数 | | jest.mock() | vi.mock() | モジュールモック | | jest.spyOn() | vi.spyOn() | スパイ | | jest.useFakeTimers() | vi.useFakeTimers() | タイマーモック | | jest.requireActual() | vi.importActual() | 実モジュールの取得 |

4-2. 自動置換スクリプト

# 一括置換(macOS / Linux)
find src -name '*.test.*' -o -name '*.spec.*' | xargs sed -i '' 's/jest\.fn/vi.fn/g'
find src -name '*.test.*' -o -name '*.spec.*' | xargs sed -i '' 's/jest\.mock/vi.mock/g'
find src -name '*.test.*' -o -name '*.spec.*' | xargs sed -i '' 's/jest\.spyOn/vi.spyOn/g'

注意jest.mock() のホイスティング挙動に依存している場合は手動調整が必要です。

4-3. モックのホイスティング問題への対処

Jest の挙動

import { getData } from './api';

jest.mock('./api'); // ← これが自動的にファイル先頭に巻き上げられる

test('should fetch data', () => {
  getData.mockResolvedValue({ id: 1 });
});

Vitest での修正

import { vi } from 'vitest';

// モックを先に宣言
vi.mock('./api', () => ({
  getData: vi.fn(),
}));

import { getData } from './api';

test('should fetch data', () => {
  vi.mocked(getData).mockResolvedValue({ id: 1 });
});

4-4. タイマーモックの移行

// Jest
jest.useFakeTimers();
setTimeout(() => console.log('done'), 1000);
jest.advanceTimersByTime(1000);
jest.useRealTimers();

// Vitest
import { vi } from 'vitest';

vi.useFakeTimers();
setTimeout(() => console.log('done'), 1000);
vi.advanceTimersByTime(1000);
vi.useRealTimers();

5. よくある移行課題と対処法

5-1. @testing-library/jest-dom のマッチャーエラー

エラー例

Property 'toBeInTheDocument' does not exist on type 'Assertion'

対処法

セットアップファイルで明示的に extend する(前述の setup.ts を参照)。

5-2. ESM パッケージのモック

// named export のモック
vi.mock('axios', () => ({
  default: {
    get: vi.fn(),
    post: vi.fn(),
  },
}));

// または動的インポートを使用
const axios = await vi.importActual<typeof import('axios')>('axios');
vi.mock('axios', () => ({
  default: {
    ...axios.default,
    get: vi.fn(),
  },
}));

5-3. グローバル変数へのアクセス

// Jest
global.fetch = jest.fn();

// Vitest
import { vi } from 'vitest';

globalThis.fetch = vi.fn();
// または
vi.stubGlobal('fetch', vi.fn());

5-4. スナップショットテストの移行

// Jest のスナップショットはそのまま使える
import { expect, test } from 'vitest';

test('matches snapshot', () => {
  const result = { id: 1, name: 'test' };
  expect(result).toMatchSnapshot();
});

注意:スナップショットファイルの形式は互換性がありますが、ファイル名が変わる場合があります(__snapshots__ ディレクトリは共通)。


6. パフォーマンス最適化と並列実行

6-1. 並列実行の設定

// vite.config.ts
export default defineConfig({
  test: {
    // ファイル並列実行(デフォルト: true)
    fileParallelism: true,
    // 単一ファイル内の並列実行(デフォルト: false)
    sequence: {
      concurrent: true,
    },
    // 最大並列ワーカー数
    maxConcurrency: 5,
  },
});

6-2. 遅いテストの特定

# レポーター設定でテスト時間を表示
npx vitest --reporter=verbose
// vite.config.ts
export default defineConfig({
  test: {
    reporters: ['verbose'],
    outputFile: './test-results.json',
  },
});

6-3. UI モードでのデバッグ

# ブラウザベースの UI でテストを実行
npx vitest --ui

利点

  • テストの依存関係を可視化
  • 個別テストの再実行が簡単
  • カバレッジをリアルタイム確認

7. CI/CD への統合

7-1. GitHub Actions の設定例

name: Test

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main, develop]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Run tests with coverage
        run: npm run test:coverage
      
      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v4
        with:
          files: ./coverage/coverage-final.json
          fail_ci_if_error: true

7-2. package.json のスクリプト

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

7-3. カバレッジしきい値の設定

// vite.config.ts
export default defineConfig({
  test: {
    coverage: {
      provider: 'v8',
      thresholds: {
        lines: 80,
        functions: 80,
        branches: 75,
        statements: 80,
      },
    },
  },
});

8. 段階的移行戦略

8-1. 推奨移行手順

| フェーズ | 作業内容 | 期間目安 | |---------|---------|----------| | 1. 検証 | 小規模なテストファイル 5〜10 件で動作確認 | 1 日 | | 2. 基盤整備 | vite.config.ts・setup.ts・CI 設定 | 2〜3 日 | | 3. 自動移行 | モック API の一括置換スクリプト実行 | 1 日 | | 4. 手動調整 | ホイスティング問題・ESM モックの修正 | 3〜5 日 | | 5. 並行運用 | Jest と Vitest を 2 週間併用 | 2 週間 | | 6. Jest 削除 | jest 関連パッケージのアンインストール | 1 日 |

8-2. 並行運用のための設定

// package.json
{
  "scripts": {
    "test:jest": "jest",
    "test:vitest": "vitest run",
    "test": "npm run test:vitest"
  }
}

メリット

  • リグレッションを早期発見
  • チーム全体の学習期間を確保
  • 問題発生時の切り戻しが容易

9. まとめ:移行判断のチェックリスト

✅ 移行前に確認すべき項目

  • [ ] Vite を使用している、または導入予定
  • [ ] テスト実行速度に課題がある(CI で 3 分以上)
  • [ ] Jest の独自機能への依存が少ない
  • [ ] チームの学習コストを吸収できる
  • [ ] 1〜2 週間の移行期間を確保できる

✅ 移行後に得られる効果

  • [ ] テスト実行時間が 50〜80% 削減
  • [ ] watch モードでの開発体験向上
  • [ ] Vite 設定との統合で管理コスト削減
  • [ ] ESM パッケージの扱いがシンプルに

✅ 移行時の注意点

  • [ ] モックのホイスティング挙動の違いを理解
  • [ ] 段階的移行で並行運用期間を設ける
  • [ ] CI/CD パイプラインの調整を忘れずに
  • [ ] カバレッジしきい値を再設定

Vitest は Jest の優れた後継ツールですが、プロジェクトの状況によっては移行コストが見合わないケースもあります。本記事の判断基準とチェックリストを参考に、適切なタイミングでの移行を検討してください。


Yureate では、テスト戦略の見直しや CI/CD パイプラインの最適化もお手伝いしています。技術選定や移行支援のご相談は、お気軽にお問い合わせください。

この内容について相談する他の記事を見る