JestでECMAScript Modulesをテストする

JestでECMAScript Modulesをテストする

package.json、TypeScript、Jestの設定を連携させ、Jestの"Cannot use import statement outside a module"エラーを解消する方法。

Takahiro Iwasa
7 min read

Jestを使ってECMAScript Modules (ESM) をテストしていると、次のようなエラーに遭遇することがあります。

SyntaxError: Cannot use import statement outside a module

これは対象のモジュールがimportキーワードを使用していることが原因です。幸い、Jestはこうしたエラーを解消するためのESMの実験的サポートを提供しています。

ESMパッケージの作成

まず、次のコマンドで新しいESMパッケージを作成します。

Terminal window
mkdir jest-esm && cd jest-esm
npm init -y

必要な開発用依存関係をインストールします。

Terminal window
npm i -D typescript jest @types/jest ts-node ts-jest

package.json"type": "module"を追加します。

package.jsonは次のようになります。

package.json
{
"name": "jest-esm",
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"devDependencies": {
"@types/jest": "^29.5.12",
"jest": "^29.7.0",
"ts-jest": "^29.1.2",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
}
}

TypeScriptの設定

次のコマンドでtsconfig.jsonファイルを生成します。

Terminal window
npx tsc --init

tsconfig.jsonファイルを更新します。

@@ -14 +14 @@
- "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
+ "target": "es6", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
@@ -28 +28 @@
- "module": "commonjs", /* Specify what module code is generated. */
+ "module": "es6", /* Specify what module code is generated. */
@@ -30 +30 @@
- // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
+ "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */

Jestの設定

次のコマンドを実行してJestの設定をセットアップします。

  • “test”スクリプトにJestを使用する: Yes
  • 設定にTypeScriptを使用する: Yes
  • テスト環境: jsdom
  • カバレッジレポートを追加する: No
  • カバレッジのプロバイダー: v8
  • モック呼び出しを自動的にクリアする…: No
Terminal window
npm init jest@latest

生成されたjest.config.tsを編集し、以下を含めます。

// preset: undefined,
preset: 'ts-jest',

ESMのサポート

Node.jsの実験的なVMモジュールを有効にするため、package.jsontestスクリプトを更新します。

"scripts": {
"test": "jest"
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
}

Jestがどのファイル形式をESMとして扱うべきかを示すため、jest.config.tsextensionsToTreatAsEsmの設定を追加します。

extensionsToTreatAsEsm: ['.ts'],

ts-jestでのESM設定については、公式ドキュメントを参照してください。

.js拡張子を扱うため、jest.config.tsmoduleNameMapperを追加して更新します。

// moduleNameMapper: {},
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1',
},

ESMをサポートするためtransform設定を更新します。

// transform: undefined,
transform: {
'^.+\\.tsx?$': [
'ts-jest',
{
useESM: true,
},
],
},

ESMの作成

デモ用に、hast-util-from-htmlパッケージをインストールします。

Terminal window
npm i hast-util-from-html

hast-util-from-htmlモジュールはECMAScript Module (ESM) であり、エクスポートにexportキーワードを使用しています。これはnode_modules/hast-util-from-html/index.jsにあるファイルを確認することで分かります。以下はそのモジュールのエクスポート構造の抜粋です。

/**
* @typedef {import('hast-util-from-parse5')} DoNotTouchItRegistersData
*
* @typedef {import('./lib/index.js').ErrorCode} ErrorCode
* @typedef {import('./lib/index.js').ErrorSeverity} ErrorSeverity
* @typedef {import('./lib/index.js').OnError} OnError
* @typedef {import('./lib/index.js').Options} Options
*/
export { fromHtml } from './lib/index.js';

index.tsを作成します。

index.ts
import { fromHtml } from 'hast-util-from-html';
export default function JestEsm(): void {
const root = fromHtml(
'<span><a href="https://github.com">GitHub</a></span>',
{ fragment: true }
);
console.info(root);
}

ESMのテスト

index.spec.tsを作成します。

index.spec.ts
import JestEsm from './index';
test('case1', () => {
JestEsm();
});

テストを実行します。

Terminal window
npm run test

次のようなエラーが表示された場合、

● Validation Error:
Test environment jest-environment-jsdom cannot be found. Make sure the testEnvironment configuration option points to an existing node module.
Configuration Documentation:
https://jestjs.io/docs/configuration
As of Jest 28 "jest-environment-jsdom" is no longer shipped by default, make sure to install it separately.

不足しているパッケージをインストールします。

Terminal window
npm i -D jest-environment-jsdom

再度テストを実行します。

Terminal window
npm run test

期待される出力は次の通りです。

PASS ./index.spec.ts
✓ case1 (20 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1 s
Ran all test suites.

まとめ

package.json、TypeScript、Jestを連携させて設定したことで、実際のESMパッケージをインポートするテストがSyntaxError: Cannot use import statement outside a moduleで失敗する状態からパスする状態に変わりました。このテストをパスさせるには、package.jsontypeフィールド、TypeScriptのモジュール設定、Jest自体の設定、そして実験的VMモジュール用のNode起動フラグという4つの異なるレイヤーにまたがる変更が必要でした。これは、「type: moduleを追加するだけ」では元のエラーが解決しない理由をよく表しています。.js拡張子のためのmoduleNameMapperの書き換えは忘れやすく、後になって誤診断されやすい部分でもあります。TypeScriptのソースコードでは拡張子なしでインポートする一方、コンパイル後のESM出力では拡張子が必要になるためです。このマッピングを削除してしまうと、元のSyntaxErrorとはまったく異なる形で失敗するため、なぜそこにあるのかを文書化しておく価値があります。

About the author

Takahiro Iwasa

Takahiro Iwasa

Software Developer

This blog shares technical notes from hands-on projects—architecture, implementation, and AWS service integrations.