🚀

サーバーサイドのESMプロジェクト(TypeScript)のテストをts-jestとesbuild-jestで実行する

2023/02/05に公開

https://zenn.dev/junkor/articles/2bcd22ca08d21d
の続き。

jestで実行する際も色々ハマりどころがあったのでメモ。

TypeScriptプロジェクトなので、ts-jestesbuild-jestのそれぞれを使ってやってみた。

検証環境

  • Linux: Ubuntu22.04LTS @WSL2(Windows11)
  • nodejs 18.13.0LTS
  • pnpm@7.26.3
    • npmやyarnを使っている人は適宜読み替えてください
  • (TypeScript: 4.9.5)

セットアップ

package.jsonで"type": "module"を設定し、tsconfig.jsonは前回記事のような感じ
で書いておく

テストコード

ESM特有の機能を使えていることを確認したいので、意味は無いがtop-level-awaitを含むテストコードがエラー無しで実行することを確認する。

tla.test.ts
/** 10msec待機した後に文字列をtop-level-awaitで取得 */
const awaitedValue: string = await (async () => {
  await new Promise((resolve) => setTimeout(resolve, 10));
  return 'awaited';
})();

test('top level await', () => {
  expect(awaitedValue).toBe('awaited');
});

ts-jestで実行

下記のパッケージをインストールする:

pnpm add -D jest @types/jest ts-jest ts-jest-resolver

ts-jest-resolverは、自作モジュールなどのimportで拡張子.jsなどをつけているのを正しく読み込むために必要。

次にjest.config.cjsを記述する。(※ESMプロジェクトなので拡張子はcjsにする必要)

jest.config.cjs
/** @type {import('jest').Config} */
const config = {
  resolver: 'ts-jest-resolver',
  extensionsToTreatAsEsm: ['.ts'],
  transform: {
    '^.+\\.(t|j)sx?$': [
      'ts-jest',
      { useESM: true }
    ],
  },
};

module.exports = config;
  • resolverで先程のts-jest-resolverを指定し、jestが正しくモジュールのimportが出来るようにする
  • extensionsToTreatAsEsmで拡張子が.tsのファイルをESMとして扱うように指定している
  • transformでts-jestを指定しており、更にオプションでuseESM: trueを指定している

ts-jestのESM Supportのページ:
https://kulshekhar.github.io/ts-jest/docs/guides/esm-support
も参照のこと。

最低限の設定は以上だが、実行の際にはまだ注意点があり、
https://jestjs.io/ja/docs/ecmascript-modules
などに記述されるように、少なくともnode18.13.0の時点ではNODE_OPTIONS=--experimental-vm-modulesを指定して実行する必要がある。

すなわち、

package.json
  "scripts": {
    // ...
+   "test": "NODE_OPTIONS='--experimental-vm-modules' jest"
  },

などのようにして、jest実行時に当該環境変数をセットして実行する必要がある。
セットしない場合、SyntaxError: Cannot use import statement outside a moduleとかSyntaxError: Cannot use 'import.meta' outside a moduleReferenceError: await is not definedなど色々とエラーが発生することがある。

とりあえず以上のような感じでなんとか動くようになる。

pnpm test path/to/tla.test.ts

> test-esm-app@1.0.0 test /home/xxxxxx/test-esm-app
> NODE_OPTIONS='--experimental-vm-modules' jest "test/unit/tla.test.ts"

(node:29140) 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  path/to/tla.test.ts
  ✓ top level await (7 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.584 s, estimated 2 s

その他注意

https://jestjs.io/ja/docs/ecmascript-modules#differences-between-esm-and-commonjs
に記載の通りだが、ESMプロジェクトをjestで実行する際にはいくつかcommonjsのときと違いがあり、例えばjestオブジェクトをimport無しで使うことが出来なかったりする。

// 公式ドキュメント例より
import { jest } from "@jest/globals"; // 明示的にimportする

jest.useFakeTimers();

// etc.

// alternatively
import.meta.jest.useFakeTimers();

// jest === import.meta.jest => true

なお、明示的にimportする場合だが、パッケージマネージャーにpnpmを使っていてかつnode-linker設定がデフォルトになっている場合はnode_modulesの構造上の問題でそのままimport {jest} from '@jest/globals'をすることが出来ない。
この場合は

  1. .npmrcにnode-linker=hoistedを設定する
  2. pnpm add -D @jest/globalsをして明示的に@jest/globalsをインストールする
  3. 上記例のalternativelyとなっている、import.meta.jest.***を使って代替する。

などの対策を取る必要が出てくる。

esbuild-jestを使う場合

ts-jestは実行が遅いので、高速化などの目的でesbuild-jestも使ってみる。
(最終更新が2021年3月で停滞気味なところは微妙だが。。。)

インストールするパッケージは以下のような感じ:

pnpm add -D jest @types/jest esbuild-jest ts-jest-resolver

ts-jestをesbuildに変えただけとなっている。

jest.config.cjsの内容は以下のような感じ:

jest.config.cjs
/** @type {import('jest').Config} */
const config = {
  resolver: 'ts-jest-resolver',
  extensionsToTreatAsEsm: ['.ts'],
  transform: {
    '^.+\\.(t|j)sx?$': [
      'esbuild-jest',
      {
        // sourceMaps: true,  // ←必要な場合
        format: 'esm',
        target: 'es2022',
      },
    ],
  },
};

module.exports = config;

ts-jestの場合と差分があるのはtransform部分だけ。

  • ts-jestでなくesbuild-jestをtransformに使うようにしている
  • format: esmを指定する
  • target: es2022以降を指定する

あたりが最低限必要になる。

それ以降はts-jestの場合と同じで、
jest実行時にNODE_OPTIONS=--experimental-vm-modulesを指定する必要があったり、jestオブジェクトの利用に関しての注意点も共通となる。

これで同じテストを実行すると、

pnpm test path/to/tla.test.ts

> test-esm-app@1.0.0 test /home/xxxxxx/test-esm-app
> NODE_OPTIONS='--experimental-vm-modules' jest "test/unit/tla.test.ts"

(node:29722) 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  path/to/tla.test.ts
  ✓ top level await (2 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.227 s, estimated 1 s

無事に動作し、実行時間が約1.5秒 → 約0.2秒と高速化されていることがわかる。
(ただし、その分型チェックはされなくなっているはずなので注意)

雑感

エコシステムの成熟度合いや先行事例の多さなどを考えてjestでセットアップする方法を調べたが、vitestもかなり良さそう。

https://vitest.dev/

jestだとESM対応はまだExperimental扱いだがvitestだとネイティブでESMサポートされているらしく?、手元で少しだけ試してみた感じでは特別な設定は不要そうだった。
こういったツールがより成長してくれると色々楽になっていくかもしれない。

GitHubで編集を提案

Discussion