•10 min read

TurborepoモノレポアーキテクチャにおけるJestからVitestへの移行

TurborepoモノレポアーキテクチャにおけるJestからVitestへの移行

エンタープライズのコードベースが大規模なTypeScriptモノレポにスケールするにつれて、テスト実行速度は開発者の生産性を決定する最大の要因となります。プルリクエストで、相互接続された数十のパッケージにわたる500以上の単体テストおよび統合テストスイートの実行が必要な場合、遅いテストパイプラインは継続的インテグレーションを麻痺させ、ローカル開発を停滞させます。

約10年間、JestはJavaScriptのデフォルトのテストフレームワークでした。しかし、Turborepoで管理されるTypeScriptモノレポと組み合わせると、Jestは深刻なアーキテクチャ上のボトルネックを露呈します。冗長なBabel/ts-jestコンパイルステップ、アプリケーションバンドラーとは別のトランスフォーマーパイプライン、そして並列テストワーカー実行中の大規模なメモリ消費です。

Viteの超高速開発サーバーとESモジュール変換パイプラインの上に直接構築されたVitestは、重複するコンパイルを排除し、桁違いに高速な実行を可能にするドロップイン代替品を提供します。

このガイドでは、プロダクションのTurborepoワークスペース内でJestからVitestへのエンドツーエンドの移行について、ワークスペース構成、モック変換、CIキャッシュ戦略を含めて説明します。


Audio Briefing
0:00 / 0:00

JestがTurborepoワークスペースのボトルネックになる理由

20個のパッケージを持つTypeScriptモノレポでは、Jestは通常、インポートされたすべてのファイルに対してts-jestまたは@babel/preset-typescriptを呼び出すことでテストを実行します。ビルドツールがVite、esbuild、またはSWCを使用してプロダクションコードをコンパイルしている場合でも、Jestは独自の分離されたトランスパイルパイプラインを維持します。

[Legacy Monorepo Pipeline]
  Build Tool (Vite / ESBuild) ──► Fast ESM Compilation ──► Production Bundle
                                       ▲
                                (Duplicated effort)
                                       ▼
  Testing Tool (Jest + ts-jest) ──► Slow CJS Compilation ──► Test Runner (High Memory)

この分割されたアーキテクチャは、3つの主要な問題点を作り出します。

  1. 冗長なコンパイル: すべてのテストワーカーが、同じ共有TypeScriptライブラリをメモリ内で独立して再コンパイルします。
  2. ESM/CommonJSの非互換性: 純粋なESM(node-fetch、chalk、またはnanoidの最新バージョンなど)を出荷するパッケージをテストするには、jest.config.jsで複雑なtransformIgnorePatterns正規表現構成が必要です。
  3. メモリリークとワーカーのオーバーヘッド: Jestの分離されたVMランナーは、大規模なテストスイートでメモリリークを引き起こし、--runInBandが強制されない限り(これにより並列処理が破壊されます)、GitHub Actionsランナーで頻繁にJavaScript heap out of memoryエラーにつながります。

Advertisement

モノレポにおけるVitestの利点

Vitestは、Viteとまったく同じ変換パイプライン、プラグインエコシステム、および構成を共有することで、これらのアーキテクチャ上の問題を解決します。

  • 単一パイプライン: VitestはViteの事前設定された変換キャッシュを使用します。Viteがパスエイリアス(@/components/*)の解決方法やTSXのコンパイル方法を知っていれば、Vitestは設定のずれなしに同じように処理します。
  • tinypoolを介したワーカープール: Vitestは、重いNodeJS子プロセスではなく軽量なワーカー スレッドを利用し、プロセス生成のオーバーヘッドを削減します。
  • ネイティブESMとViteワークスペース: VitestはECMAScriptモジュールをネイティブに理解し、vitest.workspace.tsを介したマルチプロジェクトワークスペースをネイティブにサポートします。

ステップバイステップ移行ガイド

ステップ1: Jestの依存関係をクリーンアップする

モノレポのルートからJest、ts-jest、Jest型定義、およびBabelトランスフォーマーを削除します。

# In your monorepo root
npm uninstall jest @types/jest ts-jest babel-jest
npm install -D vitest @vitest/ui @vitest/coverage-v8

ステップ2: ルートのvitest.workspace.tsを構成する

すべてのサブパッケージで分離されたテストランナーバイナリを維持するのではなく、すべてのアプリとパッケージを検出する統一されたルートワークスペースファイルを定義します。

// vitest.workspace.ts
import { defineWorkspace } from 'vitest/config';

export default defineWorkspace([
  'apps/*/vite.config.ts',
  'apps/*/vitest.config.ts',
  'packages/*/vitest.config.ts',
]);

ステップ3: 標準化されたパッケージ構成を作成する

各パッケージ(例: packages/utils/vitest.config.ts)内に、共有設定を拡張する集中的な構成を作成します。

// packages/utils/vitest.config.ts
import { defineConfig } from 'vitest/config';
import path from 'node:path';

export default defineConfig({
  test: {
    globals: true,
    environment: 'node', // Use 'happy-dom' or 'jsdom' for UI packages
    include: ['src/**/*.{test,spec}.{ts,tsx}'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      exclude: ['node_modules/**', 'dist/**'],
    },
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
});

ReactまたはNext.jsコンポーネントをテストするフロントエンドパッケージの場合、happy-dom(jsdomよりも大幅に高速です)をインストールし、次のように指定します。

// packages/ui/vitest.config.ts
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  test: {
    globals: true,
    environment: 'happy-dom',
    setupFiles: ['./src/test/setup.ts'],
  },
});

API変換のモック: jestからviへ

Vitestは、viオブジェクトを介して1対1互換のモックユーティリティを提供します。

Jest APIVitest APINotes
jest.fn()vi.fn()同じシグネチャと呼び出し追跡
jest.spyOn()vi.spyOn()完全な型保持
jest.mock()vi.mock()Vitestはvi.mockを自動的に巻き上げます
jest.useFakeTimers()vi.useFakeTimers()内部的に@sinonjs/fake-timersを使用します
jest.clearAllMocks()vi.clearAllMocks()モック履歴をクリアします

例: 非同期サービスのモック

// user-service.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { fetchUserProfile } from './user-service';

// Mock external HTTP client module
vi.mock('./api-client', () => ({
  apiClient: {
    get: vi.fn(),
  },
}));

import { apiClient } from './api-client';

describe('fetchUserProfile', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('fetches and transforms user details correctly', async () => {
    const mockUser = { id: 'usr_123', name: 'Alex Doe', email: 'alex@example.com' };
    vi.mocked(apiClient.get).mockResolvedValueOnce({ data: mockUser });

    const result = await fetchUserProfile('usr_123');

    expect(apiClient.get).toHaveBeenCalledWith('/users/usr_123');
    expect(result).toEqual({
      id: 'usr_123',
      displayName: 'Alex Doe',
    });
  });
});

Advertisement

Turborepoパイプラインキャッシュの構成

コードが変更されていない場合にテスト実行がキャッシュされるように、turbo.jsonを更新します。

{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "test": {
      "dependsOn": ["^build"],
      "inputs": ["src/**/*.ts", "src/**/*.tsx", "test/**/*.ts", "vitest.config.ts"],
      "outputs": ["coverage/**"]
    },
    "test:watch": {
      "cache": false,
      "persistent": true
    }
  }
}

これで、モノレポのルートからnpx turbo run testを実行すると、すべてのパッケージテストが並行して実行され、変更されていないパッケージでは即座にキャッシュヒットします。

# Run all tests across the monorepo
npx turbo run test

# Run tests only for a specific package and its dependents
npx turbo run test --filter=@repo/ui...

実世界のCIベンチマーク: Jest vs Vitest

以下のベンチマークは、GitHub Actions(ubuntu-latest、2 vCPU)で実行されている18個のパッケージと1,240個のテストケースを含むプロダクションモノレポから収集されました。

測定指標Jest (ts-jest)Vitest (v8プロバイダー)高速化係数
コールドCI実行 (キャッシュなし)4分18秒34秒7.6倍高速
ウォームCI実行 (Turborepoキャッシュ)2分05秒4.2秒30倍高速
ピークメモリ消費量2.1 GB RAM480 MB RAM77%メモリ削減
ウォッチモードのフィードバック遅延2,800 ms180 msほぼ瞬時

よくある質問

describeとitのインポートを避けるために、Vitestをglobals: trueで実行できますか?

はい。vitest.config.tsでtest: { globals: true }を有効にすると、describe、it、expect、およびviがグローバルに利用可能になり、Jestのデフォルトの動作と一致します。ただし、TypeScriptがグローバル識別子を認識するように、tsconfig.json compilerOptionsに"types": ["vitest/globals"]を追加する必要があります。

VitestはCommonJSのみのサードパーティライブラリをどのように処理しますか?

Viteは、esbuildを使用した自動プリバンドルによってCommonJSの依存関係を処理します。もし、テスト実行中に難解なCommonJSライブラリが解決エラーを引き起こす場合、vitest.config.tsのtest: { server: { deps: { inline: ['legacy-package-name'] } } }に明示的に追加できます。

統合されたモノレポのカバレッジレポートを生成するにはどうすればよいですか?

ルートワークスペースからvitest run --coverageを実行します。Vitestは、すべてのパッケージのカバレッジデータを統合されたV8またはIstanbulカバレッジディレクトリに収集し、CodecovまたはSonarQubeに直接アップロードできます。


こちらもおすすめ

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