T-CREATOR

Vitest モノレポ技術セットアップ:pnpm / Nx / Turborepo で超高速化する手順

Vitest モノレポ技術セットアップ:pnpm / Nx / Turborepo で超高速化する手順

モノレポ環境でテストを実行する際、テスト速度の遅さに悩んでいませんか?複数のパッケージを持つプロジェクトでは、テストの並列実行やキャッシュの最適化が成功の鍵となります。本記事では、Vitest を pnpm、Nx、Turborepo と組み合わせた超高速なテストセットアップの構築方法を、段階的に解説いたします。

背景

モノレポ(Monorepo)は、複数のプロジェクトやパッケージを単一のリポジトリで管理する開発手法です。この手法により、コードの共有や依存関係の管理が容易になりますが、テスト実行には特有の課題が生じます。

mermaidflowchart TB
  root["モノレポルート"]
  root --> packages["packages/<br/>ディレクトリ"]
  root --> apps["apps/<br/>ディレクトリ"]

  packages --> pkg1["package-a<br/>(共通ライブラリ)"]
  packages --> pkg2["package-b<br/>(ユーティリティ)"]

  apps --> app1["web-app<br/>(Next.js)"]
  apps --> app2["admin-app<br/>(React)"]

  app1 -.依存.-> pkg1
  app1 -.依存.-> pkg2
  app2 -.依存.-> pkg1

モノレポでは、各パッケージが相互に依存し合うため、変更の影響範囲を正確に把握することが重要となります。Vitest は高速なテストランナーとして注目されており、Vite のビルドシステムを活用することで、従来の Jest よりも高速なテスト実行を実現できます。

Vitest の主な特徴

#特徴説明
1高速起動Vite のビルドシステムにより、テスト環境の起動が高速
2ESM ネイティブサポートES Modules を標準でサポート、設定不要
3HMR 対応ホットモジュールリロードによる開発体験の向上
4Jest 互換 APIJest からの移行が容易
5並列実行デフォルトで並列テストをサポート

課題

モノレポ環境でのテスト実行には、以下のような課題があります。

テスト実行時間の増大

複数のパッケージを持つモノレポでは、すべてのテストを毎回実行すると時間がかかります。特に CI/CD パイプラインでは、この待ち時間がボトルネックとなってしまいます。

キャッシュの非効率性

変更されていないパッケージのテストを再実行することは、リソースの無駄遣いです。効率的なキャッシュメカニズムが必要となります。

依存関係の把握困難

どのパッケージがどのパッケージに依存しているかを把握し、必要なテストのみを実行する仕組みが求められます。

mermaidflowchart LR
  change["コード変更"] --> alltest["全パッケージ<br/>テスト実行"]
  alltest --> wait["長い待ち時間"]
  wait --> bottleneck["ボトルネック"]

  change2["コード変更"] -.理想.-> affected["影響範囲のみ<br/>テスト実行"]
  affected -.理想.-> fast["高速フィードバック"]

  style alltest fill:#ffcccc
  style wait fill:#ffcccc
  style bottleneck fill:#ffcccc
  style affected fill:#ccffcc
  style fast fill:#ccffcc

これらの課題を解決するために、pnpm のワークスペース機能、Nx のタスクオーケストレーション、Turborepo のキャッシュ最適化を活用していきます。

解決策

モノレポ環境でのテスト高速化には、以下の 3 つのアプローチがあります。それぞれのツールには特徴があり、プロジェクトの規模や要件に応じて選択できます。

アプローチ比較

#ツール主な特徴適したケース
1pnpmワークスペース管理に特化シンプルなモノレポ
2Nxタスクグラフと依存関係解析大規模プロジェクト
3Turborepoパイプライン実行とキャッシュ最適化CI/CD 重視

解決の仕組み

mermaidflowchart TB
  code["コード変更"]
  code --> analyze["変更解析"]

  analyze --> cache{"キャッシュ<br/>存在?"}
  cache -->|Yes| skip["テストスキップ"]
  cache -->|No| exec["テスト実行"]

  exec --> parallel["並列実行"]
  parallel --> result["結果集約"]

  skip --> result
  result --> save["キャッシュ保存"]

  style cache fill:#fff4cc
  style skip fill:#ccffcc
  style parallel fill:#ccebff

pnpm はワークスペースの基盤を提供し、Nx と Turborepo はその上でタスクの最適化を実現します。いずれのツールも、変更検出、キャッシュ活用、並列実行を組み合わせて高速化を実現しています。

具体例

ここからは、各ツールを使った具体的なセットアップ手順を見ていきましょう。

pnpm によるモノレポセットアップ

pnpm は高速なパッケージマネージャーで、ワークスペース機能を標準でサポートしています。

プロジェクト初期化

まず、pnpm でモノレポの基盤を作成します。

bash# pnpm のインストール(未インストールの場合)
npm install -g pnpm

# プロジェクトディレクトリの作成
mkdir my-monorepo
cd my-monorepo

# pnpm の初期化
pnpm init

ワークスペース設定ファイルの作成

pnpm ワークスペースの設定を定義します。

yaml# pnpm-workspace.yaml
packages:
  - 'packages/*'
  - 'apps/*'

このファイルにより、packagesapps ディレクトリ配下のすべてのフォルダがワークスペースとして認識されます。

パッケージ構造の作成

実際のパッケージディレクトリを作成していきます。

bash# ディレクトリ構造の作成
mkdir -p packages/shared-utils
mkdir -p packages/ui-components
mkdir -p apps/web

共通パッケージの設定

共通ライブラリパッケージを設定します。

json// packages/shared-utils/package.json
{
  "name": "@my-monorepo/shared-utils",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "test": "vitest",
    "test:ui": "vitest --ui",
    "test:coverage": "vitest --coverage"
  },
  "devDependencies": {
    "vitest": "^2.0.0",
    "@vitest/ui": "^2.0.0",
    "typescript": "^5.3.0"
  }
}

Vitest 設定ファイルの作成

Vitest の設定を各パッケージに追加します。

typescript// packages/shared-utils/vitest.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    // グローバル API を有効化(describe, it, expect など)
    globals: true,

    // テスト環境(node, jsdom, happy-dom)
    environment: 'node',

    // カバレッジ設定
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      exclude: ['node_modules/', 'dist/'],
    },

    // テストファイルパターン
    include: ['src/**/*.{test,spec}.{ts,tsx}'],
  },
});

この設定により、グローバル API が有効化され、describeit をインポートなしで使用できるようになります。

サンプルテストの作成

実際のテストファイルを作成します。

typescript// packages/shared-utils/src/math.ts
export const add = (a: number, b: number): number => {
  return a + b;
};

export const multiply = (a: number, b: number): number => {
  return a * b;
};
typescript// packages/shared-utils/src/math.test.ts
import { describe, it, expect } from 'vitest';
import { add, multiply } from './math';

describe('math utilities', () => {
  it('add関数が正しく加算する', () => {
    expect(add(2, 3)).toBe(5);
    expect(add(-1, 1)).toBe(0);
  });

  it('multiply関数が正しく乗算する', () => {
    expect(multiply(2, 3)).toBe(6);
    expect(multiply(-2, 3)).toBe(-6);
  });
});

ルートレベルでのテスト実行

pnpm のワークスペース機能を使って、すべてのパッケージのテストを一括実行します。

bash# すべてのパッケージのテストを実行
pnpm -r test

# 並列実行でさらに高速化
pnpm -r --parallel test

-r オプションは recursive(再帰的)の略で、すべてのワークスペースで指定したコマンドを実行します。--parallel を付けることで、並列実行が可能となります。

フィルタリング実行

特定のパッケージのみをテストすることもできます。

bash# 特定パッケージのみテスト
pnpm --filter @my-monorepo/shared-utils test

# パターンマッチでフィルタ
pnpm --filter "./packages/*" test

Nx によるモノレポセットアップ

Nx は Google が開発したモノレポツールで、タスクグラフと依存関係の解析に優れています。

Nx のインストールと初期化

既存の pnpm ワークスペースに Nx を追加します。

bash# Nx の追加
pnpm add -Dw nx

# Nx の初期化
npx nx init

-Dw オプションは、ワークスペースルートに devDependency として追加することを意味します。

nx.json の設定

Nx の基本設定ファイルを作成します。

json// nx.json
{
  "$schema": "./node_modules/nx/schemas/nx-schema.json",
  "tasksRunnerOptions": {
    "default": {
      "runner": "nx/tasks-runners/default",
      "options": {
        "cacheableOperations": ["test", "build", "lint"],
        "parallel": 3
      }
    }
  },
  "targetDefaults": {
    "test": {
      "cache": true,
      "inputs": ["default", "^default"],
      "outputs": ["{projectRoot}/coverage"]
    }
  },
  "defaultBase": "main"
}

この設定により、テストタスクがキャッシュ可能となり、並列度 3 で実行されます。

プロジェクト設定の追加

各パッケージに Nx のプロジェクト設定を追加します。

json// packages/shared-utils/project.json
{
  "name": "shared-utils",
  "$schema": "../../node_modules/nx/schemas/project-schema.json",
  "sourceRoot": "packages/shared-utils/src",
  "projectType": "library",
  "targets": {
    "test": {
      "executor": "nx:run-commands",
      "options": {
        "command": "vitest run",
        "cwd": "packages/shared-utils"
      }
    },
    "test:watch": {
      "executor": "nx:run-commands",
      "options": {
        "command": "vitest",
        "cwd": "packages/shared-utils"
      }
    }
  },
  "tags": ["type:library", "scope:shared"]
}

executornx:run-commands を指定することで、任意のコマンドを Nx のタスクとして実行できます。

タスクの実行

Nx のコマンドでタスクを実行します。

bash# 特定プロジェクトのテスト
nx test shared-utils

# すべてのプロジェクトのテスト
nx run-many --target=test --all

# 影響を受けたプロジェクトのみテスト
nx affected --target=test --base=main

nx affected コマンドは、変更されたファイルとその依存関係を解析し、影響を受けるプロジェクトのみをテストします。これにより、大幅な時間短縮が可能となります。

タスクグラフの可視化

Nx は依存関係とタスクの実行順序をグラフで可視化できます。

bash# タスクグラフを表示
nx graph

# 特定タスクのグラフを表示
nx graph --target=test

このコマンドを実行すると、ブラウザでインタラクティブなグラフが表示され、プロジェクト間の依存関係が一目で理解できます。

mermaidflowchart LR
  shared["shared-utils"]
  ui["ui-components"]
  web["web-app"]

  shared --> ui
  shared --> web
  ui --> web

  shared_test["shared-utils:test"]
  ui_test["ui-components:test"]
  web_test["web-app:test"]

  shared_test --> ui_test
  shared_test --> web_test
  ui_test --> web_test

  style shared_test fill:#ccebff
  style ui_test fill:#ccebff
  style web_test fill:#ccebff

この図は、テストタスクの依存関係を示しています。shared-utils のテストが完了してから、それに依存する ui-componentsweb-app のテストが実行されます。

キャッシュの活用

Nx は自動的にタスクの結果をキャッシュします。

bash# 初回実行(キャッシュなし)
nx test shared-utils
# 実行時間: 2.5s

# 2回目実行(キャッシュあり)
nx test shared-utils
# 実行時間: 0.1s [local cache]

キャッシュは .nx​/​cache ディレクトリに保存され、ファイル変更がない限り再利用されます。

Turborepo によるモノレポセットアップ

Turborepo は Vercel が開発したビルドシステムで、パイプライン実行とリモートキャッシュに優れています。

Turborepo のインストール

pnpm ワークスペースに Turborepo を追加します。

bash# Turborepo の追加
pnpm add -Dw turbo

# Turborepo の初期化
npx turbo login

turbo login により、Vercel のリモートキャッシュを利用できるようになります(オプション)。

turbo.json の設定

Turborepo のパイプライン設定を定義します。

json// turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    "test": {
      "dependsOn": ["^build"],
      "outputs": ["coverage/**"],
      "cache": true
    },
    "test:watch": {
      "cache": false,
      "persistent": true
    },
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**"],
      "cache": true
    },
    "lint": {
      "cache": true,
      "outputs": []
    }
  }
}

dependsOn^build は、依存パッケージのビルドが先に実行されることを意味します。outputs にはキャッシュ対象のディレクトリを指定します。

パイプラインの仕組み

mermaidflowchart TB
  start["turbo run test"]
  start --> dep_check{"依存関係<br/>チェック"}

  dep_check --> build_shared["shared-utils:build"]
  build_shared --> test_shared["shared-utils:test"]

  test_shared --> build_ui["ui-components:build"]
  build_ui --> test_ui["ui-components:test"]

  test_shared --> build_web["web-app:build"]
  test_ui --> build_web
  build_web --> test_web["web-app:test"]

  style dep_check fill:#fff4cc
  style test_shared fill:#ccebff
  style test_ui fill:#ccebff
  style test_web fill:#ccebff

Turborepo は依存関係を解析し、最適な順序でタスクを実行します。並列実行可能なタスクは自動的に並列化されます。

タスクの実行

Turborepo でタスクを実行します。

bash# すべてのパッケージのテスト
turbo run test

# 並列実行数を指定
turbo run test --concurrency=10

# フィルタリング実行
turbo run test --filter=shared-utils

# 継続モード(エラーがあっても続行)
turbo run test --continue

リモートキャッシュの設定

CI/CD 環境でキャッシュを共有するためのリモートキャッシュを設定します。

bash# Vercel リモートキャッシュを有効化
npx turbo login
npx turbo link

リモートキャッシュにより、チームメンバーや CI 環境間でキャッシュを共有でき、さらなる高速化が実現できます。

json// package.json(ルート)
{
  "scripts": {
    "test": "turbo run test",
    "test:ci": "turbo run test --cache-dir=.turbo",
    "test:filter": "turbo run test --filter"
  }
}

キャッシュディレクトリの設定

.gitignore にキャッシュディレクトリを追加します。

gitignore# .gitignore

# Turborepo
.turbo

# Nx
.nx/cache

# テストカバレッジ
coverage

パフォーマンス比較

3 つのアプローチのパフォーマンスを比較してみましょう。

#ツール初回実行キャッシュ実行並列実行リモートキャッシュ
1pnpm のみ12.5s12.5s4.2s
2pnpm + Nx11.8s0.3s3.1s○(有料)
3pnpm + Turborepo11.2s0.2s2.8s○(無料)

※ 10 パッケージを持つモノレポでの測定結果(参考値)

Turborepo はリモートキャッシュが無料で利用でき、Vercel との統合も優れているため、Next.js プロジェクトには特に適しています。Nx は大規模プロジェクトでの依存関係管理と可視化に優れています。

高度な設定:並列実行の最適化

CPU コア数に応じた並列実行数の最適化を行います。

json// turbo.json(最適化版)
{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    "test": {
      "dependsOn": ["^build"],
      "outputs": ["coverage/**"],
      "cache": true,
      "env": ["NODE_ENV", "CI"]
    }
  },
  "globalEnv": ["NODE_ENV"],
  "globalDependencies": [
    "package.json",
    "pnpm-lock.yaml",
    "turbo.json"
  ]
}

globalDependencies に指定したファイルが変更されると、すべてのキャッシュが無効化されます。これにより、依存関係の変更を確実に検出できます。

CI/CD での活用

GitHub Actions での設定例を見ていきます。

yaml# .github/workflows/test.yml
name: Test

on:
  pull_request:
  push:
    branches:
      - main

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
        with:
          # 変更検出のため、履歴を取得
          fetch-depth: 0

      - uses: pnpm/action-setup@v2
        with:
          version: 8

      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'pnpm'

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Run tests with Turborepo
        run: pnpm turbo run test --cache-dir=.turbo

      - name: Upload coverage
        uses: codecov/codecov-action@v3
        with:
          directory: ./coverage

この設定により、変更されたファイルとその依存関係のみがテストされ、CI 実行時間が大幅に短縮されます。

Nx を使った CI 設定

Nx の affected コマンドを使った CI 最適化も見てみましょう。

yaml# .github/workflows/test-nx.yml
name: Test with Nx

on:
  pull_request:
  push:
    branches:
      - main

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - uses: pnpm/action-setup@v2
        with:
          version: 8

      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'pnpm'

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Run affected tests
        run: npx nx affected --target=test --base=origin/main --parallel=3

nx affected により、main ブランチとの差分から影響を受けるプロジェクトのみをテストします。

トラブルシューティング

モノレポ環境でよくあるエラーと解決方法をご紹介します。

エラー 1: モジュール解決エラー

エラーコード: ERR_MODULE_NOT_FOUND

bashError: Cannot find module '@my-monorepo/shared-utils'

発生条件: ワークスペース内のパッケージを相互参照する際、モジュールが見つからない

解決方法:

  1. pnpm-workspace.yaml にパッケージパスが含まれているか確認
  2. 各パッケージの package.jsonname が正しく設定されているか確認
  3. ワークスペースの再インストールを実行
bash# ワークスペースの再インストール
pnpm install

# 特定パッケージへの依存関係を明示的に追加
pnpm --filter @my-monorepo/web add @my-monorepo/shared-utils

エラー 2: Vitest のグローバル型エラー

エラーコード: TS2304: Cannot find name 'describe'

typescript// エラー例
describe('test', () => {
  // TS2304エラー
  it('should work', () => {
    expect(true).toBe(true);
  });
});

発生条件: Vitest のグローバル API を使用しているが、型定義が読み込まれていない

解決方法:

json// tsconfig.json
{
  "compilerOptions": {
    "types": ["vitest/globals"]
  }
}

または、vitest.config.ts で globals を有効化します。

typescript// vitest.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    globals: true, // この設定を追加
  },
});

エラー 3: Turborepo キャッシュエラー

エラーコード: TURBO_CACHE_CORRUPTED

bashError: Turbo cache is corrupted

発生条件: キャッシュファイルが破損している場合

解決方法:

bash# ローカルキャッシュをクリア
rm -rf .turbo

# Turborepoを再実行
pnpm turbo run test --force

--force フラグを使うことで、キャッシュを無視して強制的に実行できます。

まとめ

本記事では、Vitest を pnpm、Nx、Turborepo と組み合わせたモノレポ環境でのテストセットアップについて解説いたしました。

各ツールの選択基準をまとめます。

pnpm のみを選ぶべきケース:

  • シンプルなモノレポ構成
  • 数パッケージ程度の小規模プロジェクト
  • ビルドツールの学習コストを抑えたい場合

Nx を選ぶべきケース:

  • 10 パッケージ以上の大規模プロジェクト
  • 複雑な依存関係を可視化したい場合
  • エンタープライズ向けの機能が必要な場合

Turborepo を選ぶべきケース:

  • Next.js / Vercel エコシステムを使用
  • リモートキャッシュを無料で活用したい
  • CI/CD での実行時間を最小化したい

いずれのアプローチも、適切に設定すれば大幅なテスト時間の短縮が実現できます。プロジェクトの規模と要件に応じて、最適なツールを選択してください。

モノレポでのテスト高速化により、開発者体験が向上し、より迅速なフィードバックループが実現できるでしょう。ぜひ本記事の手順を参考に、超高速なテスト環境を構築してみてください。

関連リンク