T-CREATOR

Jest を Yarn PnP で動かす:ゼロ‐node_modules 時代の設定レシピ

Jest を Yarn PnP で動かす:ゼロ‐node_modules 時代の設定レシピ

JavaScript のプロジェクトでテストツールとして広く使われている Jest ですが、Yarn PnP(Plug'n'Play)環境で動作させるには少し特別な設定が必要です。この記事では、従来の node_modules を使わない Yarn PnP で Jest を快適に動かす設定方法を詳しく解説していきます。

背景

Yarn PnP とは何か

Yarn PnP(Plug'n'Play)は、従来の node_modules フォルダを生成せず、パッケージの依存関係を .pnp.cjs ファイルで管理する仕組みです。この仕組みによって、インストール速度の向上やディスク容量の節約が実現できます。

通常の Node.js プロジェクトでは、パッケージをインストールすると node_modules フォルダに大量のファイルが展開されますが、PnP ではこのステップを省略し、必要なパッケージを直接 Yarn のキャッシュから読み込みます。

mermaidflowchart TB
  subgraph traditional["従来の node_modules 方式"]
    inst1["yarn install"] --> nm1["node_modules 展開"]
    nm1 --> app1["アプリケーション実行"]
  end

  subgraph pnp["Yarn PnP 方式"]
    inst2["yarn install"] --> pnpfile[".pnp.cjs 生成"]
    pnpfile --> cache["Yarn キャッシュから<br/>直接読み込み"]
    cache --> app2["アプリケーション実行"]
  end

この図が示すように、PnP 方式では中間の展開ステップを省略することで、インストール時間を大幅に短縮できます。

PnP のメリット

Yarn PnP には以下のようなメリットがあります。

#項目説明
1インストール速度node_modules への展開が不要なため、パッケージインストールが高速
2ディスク容量重複したパッケージを保存しないため、ディスク容量を節約できる
3依存関係の厳密性package.json に記載されていない依存関係へのアクセスを防ぐことができる
4再現性.pnp.cjs ファイルによって、依存関係の解決が確定的になる

特に大規模なモノレポ構成では、これらのメリットが顕著に現れるでしょう。

課題

Jest と PnP の互換性問題

Jest は元々 node_modules の存在を前提として設計されているため、Yarn PnP 環境ではそのままでは動作しません。具体的には以下のような問題が発生します。

モジュール解決の失敗

Jest がテストファイルや設定ファイルを読み込む際、通常の Node.js のモジュール解決アルゴリズムを使用します。しかし PnP 環境では、モジュールは .pnp.cjs ファイルを通じて解決される必要があるため、標準的な方法ではモジュールが見つかりません。

mermaidflowchart LR
  jest["Jest テストランナー"] -->|モジュール要求| node["Node.js 標準<br/>モジュール解決"]
  node -->|探索| nm["node_modules を探す"]
  nm -->|見つからない| error["❌ モジュール未検出"]

  style error fill:#ff9999

このフローが示すように、Jest が node_modules を探しても存在しないため、テスト実行が失敗してしまいます。

トランスフォーマーの問題

Jest は TypeScript や JSX などのファイルを変換するために、トランスフォーマー(例:babel-jestts-jest)を使用します。PnP 環境では、これらのトランスフォーマーも正しく解決できない場合があります。

主なエラーメッセージ

PnP 環境で Jest を実行すると、以下のようなエラーが表示されることがあります。

typescript// エラー例 1: モジュール解決エラー
Error: Cannot find module '@jest/globals'
Require stack:
- /path/to/test.spec.ts
typescript// エラー例 2: トランスフォーマー解決エラー
Error: Jest: Failed to parse the TypeScript config file
  Cannot find module 'ts-jest'

これらのエラーは、Jest が PnP のモジュール解決機構を理解していないために発生します。

解決策

必要なパッケージのインストール

Jest を Yarn PnP 環境で動作させるには、専用のパッケージをインストールする必要があります。

基本パッケージ

まず、Jest 本体と PnP 対応のための @yarnpkg​/​pnpify をインストールしましょう。

bash# Jest と関連パッケージをインストール
yarn add -D jest @yarnpkg/pnpify

このコマンドで、Jest の実行に必要な基本パッケージが開発用の依存関係として追加されます。

TypeScript を使用する場合

TypeScript プロジェクトでは、さらに ts-jest と型定義ファイルが必要です。

bash# TypeScript 用のパッケージを追加
yarn add -D ts-jest @types/jest typescript

ts-jest は TypeScript ファイルを Jest で実行可能な形式に変換するトランスフォーマーです。

Jest 設定ファイルの作成

次に、PnP 環境に対応した Jest の設定ファイルを作成します。プロジェクトルートに jest.config.js を配置しましょう。

JavaScript プロジェクト向け設定

JavaScript プロジェクトの場合、以下の設定で Jest を PnP 環境で動作させることができます。

javascript// jest.config.js
// PnP 環境でモジュール解決を行うための設定
module.exports = {
  // テストファイルのパターンを指定
  testMatch: [
    '**/__tests__/**/*.js',
    '**/*.test.js',
    '**/*.spec.js',
  ],

  // PnP のモジュールローダーを使用
  resolver: require.resolve('jest-pnp-resolver'),
};

この設定により、Jest は PnP のモジュール解決機構を使用するようになります。

TypeScript プロジェクト向け設定

TypeScript を使用している場合は、トランスフォーマーの設定も追加する必要があります。

javascript// jest.config.js
// TypeScript と PnP を組み合わせた設定
module.exports = {
  // テストファイルのパターン
  testMatch: [
    '**/__tests__/**/*.ts',
    '**/__tests__/**/*.tsx',
    '**/*.test.ts',
    '**/*.test.tsx',
    '**/*.spec.ts',
    '**/*.spec.tsx',
  ],

  // PnP のモジュールローダーを使用
  resolver: require.resolve('jest-pnp-resolver'),

  // TypeScript ファイルの変換設定
  transform: {
    '^.+\\.tsx?$': 'ts-jest',
  },
};

transform プロパティで、拡張子が .ts または .tsx のファイルを ts-jest で変換するように指定しています。

ts-jest の詳細設定

TypeScript の型チェックやトランスパイル設定を細かく制御したい場合は、globals セクションで ts-jest の設定を追加できます。

javascript// jest.config.js の globals セクション
module.exports = {
  // 前述の設定に加えて...
  globals: {
    'ts-jest': {
      // tsconfig.json を明示的に指定
      tsconfig: {
        // テスト実行時の TypeScript コンパイラオプション
        esModuleInterop: true,
        allowSyntheticDefaultImports: true,
      },
    },
  },
};

この設定により、テスト実行時の TypeScript コンパイラの動作を細かく制御できます。

jest-pnp-resolver のインストール

PnP 環境で Jest のモジュール解決を正しく行うには、jest-pnp-resolver が必要です。

bash# PnP リゾルバーをインストール
yarn add -D jest-pnp-resolver

このパッケージは、Jest のモジュール解決処理を PnP 対応に変換する役割を果たします。

package.json のスクリプト設定

テストを実行するためのスクリプトを package.json に追加しましょう。

json{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage"
  }
}

これらのスクリプトにより、コマンドラインから簡単にテストを実行できます。

  • yarn test: 全テストを実行
  • yarn test:watch: ファイル変更を監視して自動実行
  • yarn test:coverage: カバレッジレポートを生成

.yarnrc.yml の設定確認

Yarn PnP が正しく有効化されていることを確認します。プロジェクトルートの .yarnrc.yml ファイルを確認しましょう。

yaml# .yarnrc.yml
# PnP モードを有効化
nodeLinker: pnp

# SDK ファイルの生成(エディタ連携用)
pnpMode: loose

nodeLinker: pnp が設定されていることで、Yarn は PnP モードで動作します。

具体例

サンプルプロジェクトの構築

実際に Yarn PnP と Jest を組み合わせたプロジェクトを構築してみましょう。

プロジェクト初期化

新しいプロジェクトを作成し、Yarn PnP を有効化します。

bash# プロジェクトディレクトリを作成
mkdir jest-pnp-example && cd jest-pnp-example

# Yarn プロジェクトを初期化
yarn init -y

# PnP モードを有効化
yarn config set nodeLinker pnp

これでプロジェクトの基本構造ができました。

必要なパッケージのインストール

次に、Jest と関連パッケージをインストールします。

bash# Jest と PnP 関連パッケージをインストール
yarn add -D jest @yarnpkg/pnpify jest-pnp-resolver

# TypeScript を使う場合はさらに追加
yarn add -D typescript ts-jest @types/jest @types/node

インストールが完了すると、.pnp.cjs ファイルが生成されます。

テストファイルの作成

実際にテストを書いてみましょう。まず、テスト対象の関数を作成します。

javascript// src/calculator.js
// シンプルな計算機関数
function add(a, b) {
  return a + b;
}

function subtract(a, b) {
  return a - b;
}

function multiply(a, b) {
  return a * b;
}

function divide(a, b) {
  // ゼロ除算のチェック
  if (b === 0) {
    throw new Error('Cannot divide by zero');
  }
  return a / b;
}

module.exports = { add, subtract, multiply, divide };

次に、この関数をテストするファイルを作成します。

javascript// src/calculator.test.js
// Jest を使った単体テスト
const {
  add,
  subtract,
  multiply,
  divide,
} = require('./calculator');

describe('Calculator functions', () => {
  // 足し算のテスト
  test('adds 1 + 2 to equal 3', () => {
    expect(add(1, 2)).toBe(3);
  });

  // 引き算のテスト
  test('subtracts 5 - 3 to equal 2', () => {
    expect(subtract(5, 3)).toBe(2);
  });

  // 掛け算のテスト
  test('multiplies 4 * 5 to equal 20', () => {
    expect(multiply(4, 5)).toBe(20);
  });

  // 割り算のテスト
  test('divides 10 / 2 to equal 5', () => {
    expect(divide(10, 2)).toBe(5);
  });

  // エラーケースのテスト
  test('throws error when dividing by zero', () => {
    expect(() => divide(10, 0)).toThrow(
      'Cannot divide by zero'
    );
  });
});

これらのテストケースで、基本的な計算機能とエラーハンドリングを検証できます。

TypeScript での実装例

TypeScript を使用する場合の実装例も見てみましょう。

typescript// src/calculator.ts
// TypeScript で型安全な計算機を実装
export function add(a: number, b: number): number {
  return a + b;
}

export function subtract(a: number, b: number): number {
  return a - b;
}

export function multiply(a: number, b: number): number {
  return a * b;
}

export function divide(a: number, b: number): number {
  if (b === 0) {
    throw new Error('Cannot divide by zero');
  }
  return a / b;
}

対応するテストファイルも TypeScript で記述します。

typescript// src/calculator.test.ts
// TypeScript でのテスト実装
import {
  add,
  subtract,
  multiply,
  divide,
} from './calculator';

describe('Calculator functions with TypeScript', () => {
  test('adds 1 + 2 to equal 3', () => {
    expect(add(1, 2)).toBe(3);
  });

  test('subtracts 5 - 3 to equal 2', () => {
    expect(subtract(5, 3)).toBe(2);
  });

  test('multiplies 4 * 5 to equal 20', () => {
    expect(multiply(4, 5)).toBe(20);
  });

  test('divides 10 / 2 to equal 5', () => {
    expect(divide(10, 2)).toBe(5);
  });

  test('throws error when dividing by zero', () => {
    expect(() => divide(10, 0)).toThrow(
      'Cannot divide by zero'
    );
  });
});

TypeScript を使うことで、型チェックによるバグの早期発見が可能になります。

テストの実行

設定が完了したら、テストを実行してみましょう。

bash# テストを実行
yarn test

成功すると、以下のような出力が表示されます。

plaintext PASS  src/calculator.test.js
  Calculator functions
    ✓ adds 1 + 2 to equal 3 (2 ms)
    ✓ subtracts 5 - 3 to equal 2
    ✓ multiplies 4 * 5 to equal 20 (1 ms)
    ✓ divides 10 / 2 to equal 5
    ✓ throws error when dividing by zero (1 ms)

Test Suites: 1 passed, 1 total
Tests:       5 passed, 5 total
Snapshots:   0 total
Time:        1.234 s

このように、PnP 環境でも問題なく Jest が動作していることが確認できます。

カバレッジレポートの生成

コードカバレッジを確認したい場合は、以下のコマンドを実行します。

bash# カバレッジレポートを生成
yarn test:coverage

実行結果として、カバレッジの詳細が表示されます。

plaintext----------|---------|----------|---------|---------|-------------------
File      | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files |     100 |      100 |     100 |     100 |
 calculator.js |     100 |      100 |     100 |     100 |
----------|---------|----------|---------|---------|-------------------

カバレッジレポートは coverage ディレクトリに HTML 形式でも出力され、ブラウザで詳細を確認できます。

トラブルシューティング

PnP 環境で Jest を使用する際によくある問題と解決方法を紹介します。

モジュール解決エラーの対処

もし以下のようなエラーが発生した場合:

plaintextError: Cannot find module 'jest-pnp-resolver'
Require stack:
- /path/to/jest.config.js

これは jest-pnp-resolver が正しくインストールされていない可能性があります。

bash# パッケージを再インストール
yarn add -D jest-pnp-resolver

# Yarn のキャッシュをクリア
yarn cache clean

# 依存関係を再インストール
yarn install

これらのコマンドで依存関係を整理することで、エラーが解消されるでしょう。

エディタ連携の設定

VSCode などのエディタで TypeScript の型チェックを有効にするには、SDK ファイルを生成する必要があります。

bash# SDK ファイルを生成(VSCode 用)
yarn dlx @yarnpkg/sdks vscode

このコマンドで、.yarn​/​sdks ディレクトリに VSCode 用の設定ファイルが生成されます。

VSCode の設定で TypeScript のバージョンを選択する際、ワークスペース版を選ぶことで PnP 環境での型チェックが正しく機能します。

パフォーマンス最適化

大規模なプロジェクトでテストが遅い場合は、Jest の設定を調整してパフォーマンスを改善できます。

javascript// jest.config.js
module.exports = {
  // 前述の設定に加えて...

  // 並列実行のワーカー数を調整
  maxWorkers: '50%',

  // テストファイルのキャッシュを有効化
  cache: true,
  cacheDirectory: '.jest-cache',

  // 不要なファイルを除外
  testPathIgnorePatterns: ['/node_modules/', '/.yarn/'],
};

これらの設定により、テスト実行速度が向上する場合があります。

まとめ

この記事では、Yarn PnP 環境で Jest を動作させるための設定方法を解説しました。重要なポイントをまとめます。

設定のポイント

#項目内容
1PnP リゾルバーjest-pnp-resolver をインストールして jest.config.js で指定
2TypeScript 対応ts-jest を使用してトランスフォーマーを設定
3モジュール解決.pnp.cjs を通じた依存関係の解決を有効化
4エディタ連携SDK ファイルを生成して型チェックを有効化

Yarn PnP は従来の node_modules 方式と比べて高速かつ効率的ですが、Jest のような既存ツールとの連携には追加の設定が必要です。しかし、一度正しく設定すれば、従来と同じようにテストを実行できるようになります。

今後の展望

Yarn PnP は今後さらに普及していくと予想されます。特に大規模なモノレポや CI/CD パイプラインでは、インストール時間の短縮が大きなメリットとなるでしょう。

Jest の公式チームも PnP 対応を進めており、将来的にはより少ない設定で動作するようになる可能性があります。現時点では jest-pnp-resolver を使った設定が必要ですが、この記事で紹介した方法を使えば、すぐに PnP 環境でテストを始められますね。

Yarn PnP と Jest を組み合わせることで、モダンな JavaScript/TypeScript プロジェクトの開発体験をさらに向上させることができるでしょう。

関連リンク