JestでECMAScript Modulesをテストする
package.json、TypeScript、Jestの設定を連携させ、Jestの"Cannot use import statement outside a module"エラーを解消する方法。
Jestを使ってECMAScript Modules (ESM) をテストしていると、次のようなエラーに遭遇することがあります。
SyntaxError: Cannot use import statement outside a module
これは対象のモジュールがimportキーワードを使用していることが原因です。幸い、Jestはこうしたエラーを解消するためのESMの実験的サポートを提供しています。
ESMパッケージの作成
まず、次のコマンドで新しいESMパッケージを作成します。
mkdir jest-esm && cd jest-esmnpm init -y必要な開発用依存関係をインストールします。
npm i -D typescript jest @types/jest ts-node ts-jestpackage.jsonに"type": "module"を追加します。
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ファイルを生成します。
npx tsc --inittsconfig.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
npm init jest@latest生成されたjest.config.tsを編集し、以下を含めます。
// preset: undefined,preset: 'ts-jest',ESMのサポート
Node.jsの実験的なVMモジュールを有効にするため、package.jsonのtestスクリプトを更新します。
"scripts": { "test": "jest" "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"}Jestがどのファイル形式をESMとして扱うべきかを示すため、jest.config.tsにextensionsToTreatAsEsmの設定を追加します。
extensionsToTreatAsEsm: ['.ts'],ts-jestでのESM設定については、公式ドキュメントを参照してください。
.js拡張子を扱うため、jest.config.tsにmoduleNameMapperを追加して更新します。
// moduleNameMapper: {},moduleNameMapper: { '^(\\.{1,2}/.*)\\.js$': '$1',},ESMをサポートするためtransform設定を更新します。
// transform: undefined,transform: { '^.+\\.tsx?$': [ 'ts-jest', { useESM: true, }, ],},ESMの作成
デモ用に、hast-util-from-htmlパッケージをインストールします。
npm i hast-util-from-htmlhast-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を作成します。
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を作成します。
import JestEsm from './index';
test('case1', () => { JestEsm();});テストを実行します。
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.不足しているパッケージをインストールします。
npm i -D jest-environment-jsdom再度テストを実行します。
npm run test期待される出力は次の通りです。
PASS ./index.spec.ts ✓ case1 (20 ms)
Test Suites: 1 passed, 1 totalTests: 1 passed, 1 totalSnapshots: 0 totalTime: 1 sRan all test suites.まとめ
package.json、TypeScript、Jestを連携させて設定したことで、実際のESMパッケージをインポートするテストがSyntaxError: Cannot use import statement outside a moduleで失敗する状態からパスする状態に変わりました。このテストをパスさせるには、package.jsonのtypeフィールド、TypeScriptのモジュール設定、Jest自体の設定、そして実験的VMモジュール用のNode起動フラグという4つの異なるレイヤーにまたがる変更が必要でした。これは、「type: moduleを追加するだけ」では元のエラーが解決しない理由をよく表しています。.js拡張子のためのmoduleNameMapperの書き換えは忘れやすく、後になって誤診断されやすい部分でもあります。TypeScriptのソースコードでは拡張子なしでインポートする一方、コンパイル後のESM出力では拡張子が必要になるためです。このマッピングを削除してしまうと、元のSyntaxErrorとはまったく異なる形で失敗するため、なぜそこにあるのかを文書化しておく価値があります。
Related posts
Cognito User PoolsとOIDCでSlackサインインを実装する
Cognito user poolをOIDC経由でSlackと連携させ、"Sign in with Slack"をAmplifyでNext.jsアプリに組み込みます。
Lambda Web AdapterでFastAPIをAWS Lambdaにデプロイする
Lambda Web Adapterを使うと、FastAPIで書いたAPIバックエンドをコンテナのまま単一のLambda関数にデプロイできます。
モジュール性と再利用性を高めるReactプロジェクト構成
Reactプロジェクトをcomponents、core、features、sharedの各ディレクトリに分割し、それぞれに明確な依存方向を持たせる。
モジュール性と再利用性を高める Angular のプロジェクト構成
core、features、shared の各モジュールに依存の方向を定め、機能を独立させつつ再利用可能なコードを一箇所に集約する Angular のフォルダ構成。
Jasmine でモックオブジェクトのプロパティをスパイする
モックオブジェクトのプロパティをスパイする際に発生する Jasmine の "already been spied upon" エラーを、Object.getOwnPropertyDescriptor で回避する方法。
