できない.dev

Jest で path alias(@/)が解決できず Cannot find module になる

tsconfig.json の paths や bundler の alias は Jest には伝わらない。
Jest は独自の解決器を使うため、jest.config の moduleNameMapper に同じ対応を書き写す必要がある。
正規表現の後方参照と rootDir トークンで対応付ける。

公開: 更新:

実行例あり(2026-09-12 に実環境で検証)

要約

アプリのビルドは通るのにテストだけ Cannot find module '@/lib/foo' from 'src/app.test.ts' になるのは、@/ の対応付けが Jest に届いていないためである。

tsconfig.json の paths は TypeScript の型解決のための設定で、実行時のモジュール解決までは面倒を見ない。
webpack や Vite の resolve.alias も同様に、そのバンドラの中でしか効かない。
Jest は独自の解決器を持つので、Jest の設定(新しいタブで開く)側にも同じ対応を書き写す必要がある。

Cannot find module '@/lib/foo' from 'src/app.test.ts'

実行例

tsconfig.json に paths だけを書いた状態では別名が Jest の解決器に伝わらず、Cannot find module '@/lib/foo' でテストスイートごと失敗する。
jest.config.js に同じ対応を moduleNameMapper として書き足すと同じテストがそのまま通り、--showConfig では <rootDir> が実パスへ展開された形で効いていることまで確認できる。

$ npx jest
FAIL src/app.test.js
  ● Test suite failed to run
 
    Cannot find module '@/lib/foo' from 'src/app.test.js'
 
    > 1 | const { greet } = require('@/lib/foo');
        |                   ^
      2 |
      3 | it('greets', () => {
      4 |   expect(greet('world')).toBe('hello world');
 
      at Resolver._throwModNotFoundError (node_modules/jest-resolve/build/resolver.js:427:11)
      at Object.require (src/app.test.js:1:19)
 
Test Suites: 1 failed, 1 total
Tests:       0 total
Snapshots:   0 total
Time:        0.153 s
Ran all test suites.
終了コード: 1
$ npx jest
PASS src/app.test.js
  ✓ greets (1 ms)
 
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.15 s
Ran all test suites.
終了コード: 0
$ npx jest --showConfig | grep -A3 moduleNameMapper
"moduleNameMapper": [
        [
          "^@/(.*)$",
          "/tmp/tmp.gcnXcaMUEX/src/$1"

— 2026-09-12 時点の出力

検証環境

検証日
実行環境
node:20Debian GNU/Linux 12 (bookworm)
バージョン
  • Node.js 20.20.2
  • npm 10.8.2
  • Python 3.11.2
  • Git 2.39.5

この記事の「実行例」は、上記の環境で実際にコマンドを実行して得られた出力をそのまま掲載しています。 再現手順はリポジトリの検証スクリプトとして管理し、定期的に再実行して出力を更新しています。

よくある原因

  1. tsconfig だけ書いている: paths は型チェックのための宣言である。tsc は納得するが、Jest の実行時解決は別経路なので届かない。
  2. バンドラの alias に頼っている: resolve.alias はそのバンドラのビルド時にしか効かない。
    Jest はバンドルを経由せずファイルを読むため無関係である。
  3. パターンが緩い: '@/(.*)' のように境界を付けずに書くと、@scope/pkg のような別モジュールにも当たって解決先が壊れる。
    公式ドキュメントも境界の無い指定を避けるよう注意している。
  4. 相対パスで書いた: 置換先を ./src/$1 のように書くと、rootDir ではなく解決の基点次第でずれる。
  5. 拡張子が対象外: moduleFileExtensions に無い拡張子は、対応付けが正しくても見つからない。

解決策

1. moduleNameMapper に写す

// jest.config.js
module.exports = {
  moduleNameMapper: {
    "^@/(.*)$": "<rootDir>/src/$1",
    "^~/components/(.*)$": "<rootDir>/src/components/$1",
  },
};

tsconfig.json の "@/*": ["src/*"] に 1 対 1 で対応させる。$1 は正規表現の後方参照で、<rootDir> は Jest が設定ファイルの位置から決めるプロジェクトルートである。

2. 境界を付ける

// NG: @scope/pkg にも当たる
"@/(.*)": "<rootDir>/src/$1",
 
// OK: 先頭が @/ のときだけ
"^@/(.*)$": "<rootDir>/src/$1",

対応付けは上から順に評価され、最初に一致したものが使われる。
広いパターンを先に置かない。

3. スタイルや画像はスタブへ

moduleNameMapper: {
  "^@/(.*)$": "<rootDir>/src/$1",
  "\\.(css|scss)$": "<rootDir>/test/styleStub.js",
  "\\.(png|jpg|svg)$": "<rootDir>/test/fileStub.js",
},

CSS を読み込めずに落ちているのか、エイリアスが解けていないのかを切り分けやすくなる。

4. 実際に効いている設定を見る

npx jest --showConfig | grep -A 10 moduleNameMapper

設定ファイルが複数ある構成では、意図した方が読まれていないことがある。
まず効いている値を確認してから直す。

この記事は役立ちましたか?