T-CREATOR

Jest moduleNameMapper 早見表:パスエイリアス/静的アセット/CSS を一網打尽

Jest moduleNameMapper 早見表:パスエイリアス/静的アセット/CSS を一網打尽

Jest でテストを書いていると、パスエイリアスや静的ファイルのインポートでエラーに遭遇することがよくあります。そんなとき頼りになるのが moduleNameMapper 設定です。この記事では、実務で即使える設定パターンを早見表とともにご紹介します。

moduleNameMapper 設定早見表

以下の表は、よく使われる moduleNameMapper の設定パターンをまとめたものです。プロジェクトに応じて参考にしてください。

#用途設定キー(正規表現)マッピング先備考
1パスエイリアス @​/​^@​/​(.*)$<rootDir>​/​src​/​$1TypeScript の paths と対応
2パスエイリアス ~​/​^~​/​(.*)$<rootDir>​/​$1プロジェクトルートを指す
3CSS ファイル\\.css$identity-obj-proxyCSS Modules のモック
4CSS Modules\\.module\\.css$identity-obj-proxyclassName を文字列として返す
5SCSS/Sass\\.(scss|sass)$identity-obj-proxyCSS と同様にモック
6画像ファイル\\.(jpg|jpeg|png|gif|svg)$<rootDir>​/​__mocks__​/​fileMock.js静的アセットのモック
7フォント\\.(woff|woff2|eot|ttf|otf)$<rootDir>​/​__mocks__​/​fileMock.jsフォントファイルのモック
8静的ファイル全般\\.(css|less|scss|sass|styl)$identity-obj-proxyスタイル系を一括モック
9node_modules 特定パッケージ^lodash-es$lodashESM を CommonJS に置換
10JSON ファイル\\.json$<rootDir>​/​__mocks__​/​jsonMock.js必要に応じてモック

背景

Jest とモジュール解決の課題

Jest は Node.js 環境でテストを実行するため、ブラウザ専用の機能や Webpack、Vite などのバンドラー固有の機能をそのままでは理解できません。

現代のフロントエンド開発では、以下のような便利な機能が当たり前に使われています。

  • パスエイリアス: import Button from '@​/​components​/​Button' のような短縮記法
  • CSS Modules: import styles from '.​/​Button.module.css' でスタイルをインポート
  • 静的アセット: import logo from '.​/​logo.png' で画像パスを取得
  • ESM パッケージ: lodash-es のような ES Modules 形式のライブラリ

しかし、Jest はこれらをデフォルトでは解決できないため、テスト実行時にエラーが発生してしまいます。

moduleNameMapper の役割

moduleNameMapper は、モジュールのインポートパスを Jest が理解できる形式に変換するための設定です。正規表現でパターンマッチし、適切なモックファイルや実際のパスに置き換えることで、テストを正常に動作させます。

以下の図は、moduleNameMapper がどのようにインポート文を変換するかを示しています。

mermaidflowchart LR
  original["ソースコード<br/>import '@/utils/helper'"] -->|Jest 実行| mapper["moduleNameMapper"]
  mapper -->|正規表現マッチ| pattern["^@/(.*)$"]
  pattern -->|変換| resolved["&lt;rootDir&gt;/src/utils/helper"]
  resolved --> test["テスト実行成功"]

図で理解できる要点:

  • インポート文が正規表現でマッチングされる
  • マッピング設定に従って実際のパスに変換される
  • Jest がモジュールを正しく解決できるようになる

課題

よくあるエラーとその原因

moduleNameMapper の設定不足や誤りにより、以下のようなエラーが発生します。

エラー 1: パスエイリアスが解決できない

エラーコード: Cannot find module '@​/​components​/​Button' from 'App.test.tsx'

typescript// App.test.tsx
import { Button } from '@/components/Button'; // ❌ Jest が '@/' を理解できない

発生条件:

  • TypeScript の tsconfig.json でパスエイリアスを設定している
  • Jest 側で対応する moduleNameMapper が未設定

エラー 2: CSS Modules のインポートエラー

エラーコード: Jest encountered an unexpected token

cssSyntaxError: Unexpected token '.'
  .button {
  ^

発生条件:

  • CSS ファイルを直接インポートしている
  • Jest が CSS の構文を解析しようとしてしまう
typescript// Button.tsx
import styles from './Button.module.css'; // ❌ Jest が CSS を JavaScript として解析

エラー 3: 画像ファイルのインポートエラー

エラーコード: SyntaxError: Invalid or unexpected token

typescript// Logo.tsx
import logo from './logo.png'; // ❌ Jest がバイナリファイルを解析できない

発生条件:

  • 静的アセットをモジュールとしてインポートしている
  • 画像ファイルのモックが未設定

以下の図は、これらのエラーがどのように発生するかを示しています。

mermaidflowchart TD
  import["ソースコード内の import 文"] --> check{"moduleNameMapper<br/>にマッチ?"}
  check -->|Yes| mock["モック/変換後のパス"]
  check -->|No| error["❌ エラー発生"]
  mock --> testok["✓ テスト実行"]
  error --> error1["Cannot find module"]
  error --> error2["Unexpected token"]
  error --> error3["Invalid token"]

図で理解できる要点:

  • moduleNameMapper にマッチしないとエラーが発生
  • 適切な設定がテスト成功の鍵となる

解決策

基本的な設定構造

jest.config.js または jest.config.tsmoduleNameMapper を記述します。

javascript// jest.config.js の基本構造
module.exports = {
  moduleNameMapper: {
    // キー: 正規表現パターン
    // 値: 変換先のパスまたはモジュール
    '^@/(.*)$': '<rootDir>/src/$1',
  },
};

設定のポイント:

  • キーは正規表現の文字列(バックスラッシュのエスケープに注意)
  • <rootDir> は Jest のルートディレクトリを指す特殊変数
  • $1 はキャプチャグループ(括弧内)にマッチした部分

パスエイリアスの設定

TypeScript や Webpack で設定したパスエイリアスを Jest に認識させます。

tsconfig.json の設定例

json{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "~/*": ["./*"]
    }
  }
}

この設定に対応する moduleNameMapper は以下のようになります。

jest.config.js での対応設定

javascriptmodule.exports = {
  moduleNameMapper: {
    // '@/' を 'src/' に変換
    '^@/(.*)$': '<rootDir>/src/$1',

    // '~/' をプロジェクトルートに変換
    '^~/(.*)$': '<rootDir>/$1',
  },
};

設定の読み方:

  • ^@​/​(.*)$: @​/​ で始まり、その後に任意の文字列が続くパターン
  • <rootDir>​/​src​/​$1: @​/​<rootDir>​/​src​/​ に置き換え、$1 に残りの部分を適用

CSS と CSS Modules の設定

スタイルファイルのインポートをモックに置き換えます。

CSS Modules 用のモック設定

javascriptmodule.exports = {
  moduleNameMapper: {
    // CSS Modules(.module.css)を identity-obj-proxy でモック
    '\\.module\\.(css|scss|sass)$': 'identity-obj-proxy',

    // 通常の CSS ファイルもモック
    '\\.(css|scss|sass|less|styl)$': 'identity-obj-proxy',
  },
};

identity-obj-proxy とは:

  • CSS Modules の className をそのまま文字列として返すモックライブラリ
  • styles.button"button" のように変換されるため、テストでクラス名の検証が可能

インストール方法

bashyarn add -D identity-obj-proxy

テストでの使用例

typescript// Button.test.tsx
import { render } from '@testing-library/react';
import Button from './Button';

test('ボタンに正しいクラス名が適用されている', () => {
  const { container } = render(<Button />);
  const button = container.querySelector('.button');

  // identity-obj-proxy により、className が文字列として取得可能
  expect(button).toBeInTheDocument();
});

静的アセット(画像・フォント)の設定

画像やフォントファイルをモックファイルで置き換えます。

モックファイルの作成

まず、静的ファイル用のモックを作成します。

javascript// __mocks__/fileMock.js
module.exports = 'test-file-stub';

このシンプルなモックは、すべての静的ファイルを文字列 'test-file-stub' として扱います。

jest.config.js での設定

javascriptmodule.exports = {
  moduleNameMapper: {
    // 画像ファイルをモックに置換
    '\\.(jpg|jpeg|png|gif|svg|webp)$':
      '<rootDir>/__mocks__/fileMock.js',

    // フォントファイルをモックに置換
    '\\.(woff|woff2|eot|ttf|otf)$':
      '<rootDir>/__mocks__/fileMock.js',
  },
};

正規表現の解説:

  • \\. : ドット(.)をエスケープ
  • (jpg|jpeg|png|...) : OR 条件で複数の拡張子にマッチ
  • $ : 文字列の終端

テストでの検証例

typescript// Logo.test.tsx
import logo from './logo.png';

test('ロゴのパスが取得できる', () => {
  // logo は 'test-file-stub' という文字列になる
  expect(logo).toBe('test-file-stub');
});

ESM パッケージの対応

一部の npm パッケージは ES Modules(ESM)形式でのみ提供されていますが、Jest は CommonJS を前提としているため、そのままでは動作しません。

よくある ESM パッケージの例

  • lodash-es
  • nanoid
  • uuid (v9 以降)

エラー例

エラーコード: SyntaxError: Cannot use import statement outside a module

javascriptexport { default as debounce } from './debounce.js';
^^^^^^
SyntaxError: Cannot use import statement outside a module

moduleNameMapper での解決方法

javascriptmodule.exports = {
  moduleNameMapper: {
    // ESM 版を CommonJS 版にマッピング
    '^lodash-es$': 'lodash',

    // nanoid の ESM を CommonJS に置換
    '^nanoid$': require.resolve('nanoid'),
  },
};

別の解決方法:

transformIgnorePatterns を使って ESM パッケージを変換対象に含める方法もあります。

javascriptmodule.exports = {
  transformIgnorePatterns: [
    'node_modules/(?!(lodash-es|nanoid)/)',
  ],
};

具体例

Next.js プロジェクトの実践的な設定

Next.js では、パスエイリアス、CSS Modules、画像最適化など、複数の機能が組み合わさっています。それらすべてに対応した設定例をご紹介します。

プロジェクト構成

cssmy-nextjs-app/
├── src/
│   ├── components/
│   │   ├── Button/
│   │   │   ├── Button.tsx
│   │   │   ├── Button.module.css
│   │   │   └── Button.test.tsx
│   │   └── Header/
│   │       ├── Header.tsx
│   │       └── logo.svg
│   ├── utils/
│   │   └── helper.ts
│   └── app/
│       └── page.tsx
├── __mocks__/
│   └── fileMock.js
├── jest.config.js
├── tsconfig.json
└── package.json

tsconfig.json の設定

json{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"],
      "@components/*": ["./src/components/*"],
      "@utils/*": ["./src/utils/*"]
    }
  }
}

この設定により、ソースコード内で以下のようなインポートが可能になります。

typescript// パスエイリアスを使ったインポート例
import { Button } from '@/components/Button/Button';
import { formatDate } from '@utils/helper';

完全な jest.config.js

以下は、上記のプロジェクト構成に対応した jest.config.js の完全版です。

javascriptmodule.exports = {
  // テスト環境の指定
  testEnvironment: 'jsdom',

  // ルートディレクトリ
  roots: ['<rootDir>/src'],

  // モジュール名のマッピング(優先順位が重要)
  moduleNameMapper: {
    // 1. パスエイリアス(具体的なものから先に記述)
    '^@components/(.*)$': '<rootDir>/src/components/$1',
    '^@utils/(.*)$': '<rootDir>/src/utils/$1',
    '^@/(.*)$': '<rootDir>/src/$1',

    // 2. CSS Modules(.module.css を先にマッチさせる)
    '\\.module\\.(css|scss|sass)$': 'identity-obj-proxy',

    // 3. 通常の CSS ファイル
    '\\.(css|scss|sass|less)$': 'identity-obj-proxy',

    // 4. 画像ファイル
    '\\.(jpg|jpeg|png|gif|svg|webp|ico)$':
      '<rootDir>/__mocks__/fileMock.js',

    // 5. フォントファイル
    '\\.(woff|woff2|eot|ttf|otf)$':
      '<rootDir>/__mocks__/fileMock.js',
  },

  // ファイル拡張子の解決順序
  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],

  // 変換対象外のパターン
  transformIgnorePatterns: [
    'node_modules/(?!(nanoid|lodash-es)/)',
  ],

  // テストファイルのパターン
  testMatch: [
    '**/__tests__/**/*.[jt]s?(x)',
    '**/?(*.)+(spec|test).[jt]s?(x)',
  ],
};

設定のポイント:

  • moduleNameMapper の記述順序が重要(より具体的なパターンを先に)
  • CSS Modules の .module.css を通常の .css より先に記述
  • ESM パッケージは transformIgnorePatterns で変換対象に含める

mocks/fileMock.js の内容

javascript// __mocks__/fileMock.js
// 静的アセットのモック(画像、フォントなど)
module.exports = 'test-file-stub';

コンポーネントの実装例

typescript// src/components/Button/Button.tsx
import styles from './Button.module.css';

interface ButtonProps {
  label: string;
  onClick?: () => void;
}

export const Button: React.FC<ButtonProps> = ({
  label,
  onClick,
}) => {
  return (
    <button className={styles.button} onClick={onClick}>
      {label}
    </button>
  );
};

このコンポーネントに対するテストを書いてみます。

テストコードの実装例

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

describe('Button コンポーネント', () => {
  test('ラベルが正しく表示される', () => {
    render(<Button label='送信' />);

    // ボタンのテキストを検証
    expect(screen.getByText('送信')).toBeInTheDocument();
  });

  test('クリックイベントが発火する', () => {
    const handleClick = jest.fn();
    render(
      <Button label='クリック' onClick={handleClick} />
    );

    // ボタンをクリック
    fireEvent.click(screen.getByText('クリック'));

    // ハンドラーが呼ばれたことを検証
    expect(handleClick).toHaveBeenCalledTimes(1);
  });
});

このテストは、moduleNameMapper により CSS Modules が適切にモックされているため、エラーなく実行されます。

TypeScript + Vite プロジェクトの設定例

Vite を使用したプロジェクトでも、同様に moduleNameMapper の設定が必要です。

vite.config.ts でのエイリアス設定

typescript// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
      '@assets': path.resolve(__dirname, './src/assets'),
    },
  },
});

この Vite の設定に対応する Jest の設定は以下のようになります。

対応する jest.config.ts

typescript// jest.config.ts
import type { Config } from 'jest';

const config: Config = {
  preset: 'ts-jest',
  testEnvironment: 'jsdom',

  moduleNameMapper: {
    // Vite のエイリアスと同じ設定
    '^@/(.*)$': '<rootDir>/src/$1',
    '^@assets/(.*)$': '<rootDir>/src/assets/$1',

    // スタイルファイル
    '\\.module\\.(css|scss)$': 'identity-obj-proxy',
    '\\.(css|scss)$': 'identity-obj-proxy',

    // 静的アセット
    '\\.(png|jpg|jpeg|gif|svg|webp)$':
      '<rootDir>/__mocks__/fileMock.js',
  },

  transform: {
    '^.+\\.tsx?$': 'ts-jest',
  },
};

export default config;

画像インポートを含むコンポーネント例

typescript// src/components/Header/Header.tsx
import logo from '@assets/logo.svg';
import styles from './Header.module.css';

export const Header: React.FC = () => {
  return (
    <header className={styles.header}>
      <img src={logo} alt='ロゴ' className={styles.logo} />
      <h1>My App</h1>
    </header>
  );
};

このコンポーネントのテストでは、画像ファイルが適切にモックされます。

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

test('ヘッダーにロゴが表示される', () => {
  render(<Header />);

  const logo = screen.getByAltText('ロゴ');

  // モックされた画像パスが設定されている
  expect(logo).toHaveAttribute('src', 'test-file-stub');
});

複雑なケース: SVG を React コンポーネントとして扱う

Next.js や Vite では、SVG を React コンポーネントとしてインポートできます。

typescript// ソースコード内での SVG インポート
import { ReactComponent as Logo } from './logo.svg';

// 使用例
<Logo width={100} height={100} />;

この場合、moduleNameMapper での対応が少し複雑になります。

SVG コンポーネント用のモック作成

javascript// __mocks__/svgMock.js
import React from 'react';

const SvgMock = React.forwardRef((props, ref) => (
  <svg ref={ref} {...props} />
));

SvgMock.displayName = 'SvgMock';

export default SvgMock;
export { SvgMock as ReactComponent };

このモックは、SVG を React コンポーネントとして扱えるようにします。

jest.config.js での設定

javascriptmodule.exports = {
  moduleNameMapper: {
    // SVG を React コンポーネントとして扱う場合
    '\\.svg$': '<rootDir>/__mocks__/svgMock.js',

    // その他の画像は通常のモック
    '\\.(png|jpg|jpeg|gif|webp)$':
      '<rootDir>/__mocks__/fileMock.js',
  },
};

テストでの使用例

typescript// Logo.test.tsx
import { render } from '@testing-library/react';
import { ReactComponent as Logo } from './logo.svg';

test('SVG コンポーネントがレンダリングされる', () => {
  const { container } = render(<Logo />);

  // モックされた SVG 要素が存在することを確認
  const svg = container.querySelector('svg');
  expect(svg).toBeInTheDocument();
});

まとめ

Jest の moduleNameMapper は、モダンなフロントエンド開発に欠かせない設定です。パスエイリアス、CSS Modules、静的アセットなど、様々なインポート形式に対応することで、快適なテスト環境が実現できます。

本記事でご紹介した設定パターンは、以下のような場面で役立ちます。

重要なポイント:

  • パスエイリアス: TypeScript や Webpack の設定と一致させることで、ソースコードとテストで同じインポート記法が使える
  • CSS Modules: identity-obj-proxy を使うことで、クラス名の検証が可能になる
  • 静的アセット: モックファイルで置き換えることで、バイナリファイルの解析エラーを回避できる
  • 設定の順序: より具体的なパターンを先に記述することで、正しくマッチングされる
  • ESM 対応: transformIgnorePatterns と組み合わせて、ESM パッケージにも対応可能

適切な moduleNameMapper の設定により、「テストが通らない」というストレスから解放され、本来のテストロジックに集中できるようになるでしょう。

早見表を手元に置いておけば、新しいプロジェクトでもすぐに設定を完了できます。ぜひ、この記事をブックマークして、実務でご活用ください。

関連リンク