Jest で ECMAScript Modules (ESM) をテスト

Jest で ECMAScript Modules (ESM) をテスト

Takahiro Iwasa
(岩佐 孝浩)
Takahiro Iwasa (岩佐 孝浩)
8 min read
Jest

Jest で ECMAScript Modules (ESM) をテストしている時に、 SyntaxError: Cannot use import statement outside a module というエラーが発生しました。

このエラーは、テスト対象が import キーワードで他のモジュールを利用していることが原因です。 Jest experimental support を利用することで、このエラーを解決できます。

Jest ships with experimental support for ECMAScript Modules (ESM).

この記事では、 ESM パッケージの例を用いて、公式ドキュメントを参考にどのように解決するかを紹介します。

プロジェクト初期化

ESM パッケージ作成

以下のコマンドを実行して、 ESM パッケージを作成してください。

mkdir jest-esm && cd jest-esm
npm init # Run with all options default

以下のパッケージをインストールしてください。

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

生成された package.json は以下のとおりです。

{
  "name": "jest-esm",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "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"
  }
}

"type": "module"package.json に追加してください。

@@ -3,6 +3,7 @@
   "version": "1.0.0",
   "description": "",
   "main": "index.js",
+  "type": "module",
   "scripts": {
     "test": "echo \"Error: no test specified\" && exit 1"
   },

TypeScript 設定

以下のコマンドを実行して、 tsconfig.json を作成してください。

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.config.ts) を生成してください。

npm init jest@latest

The following questions will help Jest to create a suitable configuration for your project

✔ Would you like to use Jest when running "test" script in "package.json"? … yes
✔ Would you like to use Typescript for the configuration file? … yes
✔ Choose the test environment that will be used for testing › jsdom (browser-like)
✔ Do you want Jest to add coverage reports? … no
✔ Which provider should be used to instrument code for coverage? › v8
✔ Automatically clear mock calls, instances, contexts and results before every test? … no

指定したオプションは以下のテーブルのとおりです。

OptionValue
use Jest when running “test” script in “package.json”yes
use Typescript for the configuration fileyes
test environmentjsdom (browser-like)
add coverage reportsno
provider for coveragev8
Automatically clear mock calls, instances, contexts and resultsno

ts-jest 設定

公式ドキュメントに記載のとおり、 ts-jestjest.config.tspreset に設定してください。

Instead, add the line: preset: “ts-jest” to the jest.config.js file afterwards.

@@ -102,7 +102,7 @@
   // notifyMode: "failure-change",

   // A preset that is used as a base for Jest's configuration
-  // preset: undefined,
+  preset: 'ts-jest',

   // Run tests from one or more projects
   // projects: undefined,

Jest の ESM サポート

2024年2月現在、 Jest ESM サポートは Experimental です。詳細は、公式ドキュメントをご参照ください。

Jest ships with experimental support for ECMAScript Modules (ESM).

The implementation may have bugs and lack features. For the latest status check out the issue and the label on the issue tracker.

Also note that the APIs Jest uses to implement ESM support are still considered experimental by Node (as of version 18.8.0).

package.json を以下のように修正してください。

@@ -5,7 +5,7 @@
   "main": "index.js",
   "type": "module",
   "scripts": {
-    "test": "jest"
+    "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
   },
   "author": "",
   "license": "ISC",

次に、 extensionsToTreatAsEsmjest.config.ts に追加してください。

@@ -194,6 +194,8 @@

   // Whether to use watchman for file crawling
   // watchman: true,
+
+  extensionsToTreatAsEsm: ['.ts'],
 };

 export default config;

ts-jest の ESM サポート

公式ドキュメントに基づいて、 jest.config.ts を以下のように更新してください。

@@ -90,7 +90,9 @@
   // ],

   // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
-  // moduleNameMapper: {},
+  moduleNameMapper: {
+    '^(\\.{1,2}/.*)\\.js$': '$1',
+  },

   // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
   // modulePathIgnorePatterns: [],
@@ -175,7 +177,14 @@
   // testRunner: "jest-circus/runner",

   // A map from regular expressions to paths to transformers
-  // transform: undefined,
+  transform: {
+    '^.+\\.tsx?$': [
+      'ts-jest',
+      {
+        useESM: true,
+      },
+    ],
+  },

   // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
   // transformIgnorePatterns: [

ESM 作成

hast-util-from-html インストール

サードパーティの ESM として、 hast-util-from-html をインストールしてください。

npm i hast-util-from-html
@@ -15,5 +15,8 @@
     "ts-jest": "^29.1.2",
     "ts-node": "^10.9.2",
     "typescript": "^5.3.3"
+  },
+  "dependencies": {
+    "hast-util-from-html": "^2.0.1"
   }
 }

node_modules/hast-util-from-html/index.js を確認してください。 export キーワードでエクスポートされています。

/**
 * @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'

ESM

以下の内容で index.ts を作成してください。 hast-util-from-html から fromHtml をインポートしています。

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);
}

テストコードとして、以下の内容で index.spec.ts を作成してください。

import JestEsm from './index';

test('case1', () => {
  JestEsm();
});

ESM テスト

以下のコマンドを実行して、モジュールをテストしてください。

npm run test

> [email protected] test
> jest

● 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.

出力された情報に基づいて、 jest-environment-jsdom をインストールしてください。

npm i -D jest-environment-jsdom
@@ -12,6 +12,7 @@
   "devDependencies": {
     "@types/jest": "^29.5.12",
     "jest": "^29.7.0",
+    "jest-environment-jsdom": "^29.7.0",
     "ts-jest": "^29.1.2",
     "ts-node": "^10.9.2",
     "typescript": "^5.3.3"

再度テストを実行してください。テストが成功するはずです。

npm run test

> [email protected] test
> node --experimental-vm-modules node_modules/jest/bin/jest.js

  console.info
    {
      type: 'root',
      children: [
        {
          type: 'element',
          tagName: 'span',
          properties: {},
          children: [Array],
          position: [Object]
        }
      ],
      data: { quirksMode: false },
      position: {
        start: { line: 1, column: 1, offset: 0 },
        end: { line: 1, column: 53, offset: 52 }
      }
    }

      at JestEsm (index.ts:8:11)

(node:47304) ExperimentalWarning: VM Modules is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
 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.
Takahiro Iwasa
(岩佐 孝浩)

Takahiro Iwasa (岩佐 孝浩)

Software Developer at KAKEHASHI Inc.
処方箋データ収集基盤の設計・開発・運用に携わっています。