🔎

JestでJSDOM環境を使ってテストすると"structuredClone is not defined"が出るときの対処法

に公開

概要

先日 Jest を使ってテストをする際に ReferenceError: structuredClone is not defined というエラーが発生してしまい、少々時間がかかりましたが解決しました。

そこで、本記事ではエラーが発生した原因や対処法などを備忘録としてまとめました。
本記事に載せた方法で必ず対処できるとは限りませんが、皆さんの問題解決の一助となれば幸いです。

1. エラーを再現してみる

エラーを再現できる最低限の構成にしたリポジトリで説明します。

/
├─ src
│  └─ script.ts
├─ tests
│  └─ script.test.ts
├─ jest.config.js
├─ package.json
└─ tsconfig.json

パッケージ管理ツールには Yarn (v1.22.22) を使いますが、npm でも同じように動作すると思います。

下準備

まず package.jsontsconfig.json に次のコードを書きます。

package.json
{
  "name": "jsdom-override-test",
  "license": "UNLICENSED",
  "devDependencies": {
    "@types/jest": "^30.0.0",
    "jest": "^30.0.4",
    "jest-environment-jsdom": "^30.0.4",
    "ts-jest": "^29.4.0",
    "typescript": "^5.8.3"
  }
}
tsconfig.json
{
  "compilerOptions": {
    "target": "es2022",
    "module": "esnext",
    "lib": ["es2022", "dom"],
    "rootDir": "./src",
    "outDir": "./dist",
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "moduleResolution": "node",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true
  },
  "include": ["src/**/*"],
  "exclude": ["dist", "node_modules"]
}

その他のファイルは空ファイルのままでよいです。

次に yarn install でパッケージをインストールします。パッケージを保存する node_modules ディレクトリと、パッケージの依存関係を管理する yarn.lock ファイルが作成されます。

インストールしたら、yarn tsc でコンパイルが実行できることを確認します。コンパイルに成功すると dist ディレクトリが作成されます。

テストの準備

Jest の設定ファイル jest.config.js に次のコードを書きます。

jest.config.js
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'jsdom',
  roots: ['<rootDir>/tests'],
};

preset: 'ts-jest' で Jest が TypeScript に対応するようになり、testEnvironment: 'jsdom' でテスト環境を JSDOM (Node.js 環境でブラウザの DOM や Web API を利用するためのライブラリ) に設定します。


テスト対象のコード script.ts と、これをテストするための script.test.ts を用意します。

ここでは structuredClone を使った結果をそのまま返す簡単な関数を用意し、これを Jest でテストすることにします。

script.ts
export function copyObject<T>(original: T): T {
    return structuredClone(original);
}
script.test.ts
import { copyObject } from '../src/script';

describe('テスト', () => {
    test('オブジェクトを複製する', () => {
        const original = { a: 1, b: { ba: 2, bb: 3 }};
        const copied = copyObject(original);

        // コピーは深い階層も含めてオリジナルとは異なるオブジェクトを参照している
        expect(copied).not.toBe(original);
        expect(copied.b).not.toBe(original.b);
        // 内容は同じ(値同士の比較は真)
        expect(copied).toEqual(original);
    });
});

テストするとエラー発生

コードを書いたら yarn jest でテストを実行します。すると、次のようにテストは失敗してしまい、ReferenceError: structuredClone is not defined というエラーが発生します。

PS C:\...\JsdomOverrideTest> yarn jest
yarn run v1.22.22
$ C:\...\JsdomOverrideTest\node_modules\.bin\jest
 FAIL  tests/script.test.ts
  テスト
    × オブジェクトを複製する (2 ms)

  ● テスト › オブジェクトを複製する

    ReferenceError: structuredClone is not defined

      1 | export function copyObject<T>(original: T): T {
    > 2 |     return structuredClone(original);
        |     ^
      3 | }
      4 |

      at copyObject (src/script.ts:2:5)
      at Object.<anonymous> (tests/script.test.ts:6:32)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        2.849 s
Ran all test suites.
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

2. エラーが発生する原因

第一にエラーが発生する直接の原因は、ReferenceError: structuredClone is not defined を直訳すればわかるように「structuredClone が定義されていない」ことです。

そもそも structuredClone が何者かというと、これは Web API の1つでオブジェクトの ディープコピー をおこなえる関数です。
同様にオブジェクトのディープコピーをおこなえる関数として JSON.parse(JSON.stringify(value)) がありますが、こちらは JSON 文字列に変換できるオブジェクトにしか対応していません。
一方、structuredClone はプリミティブ型に加えて Date 型・File 型・Map 型など、より多種類のオブジェクトのコピーに対応しています[1]

では、なぜ structuredClone が定義されていないのかというと、Jest 上で JSDOM 環境を利用するためのライブラリ jest-environment-jsdomstructuredClone に対応していない からです。
JSDOM ではブラウザと Node.js の両方で古いバージョンから利用できた機能には対応しているものの、RequestTextEncoderstructuredClone など一部の (比較的最近追加された?) グローバル変数や Web API には対応しておらず、これらの機能を利用しようとするとエラーが発生するようです[2]

そのため、現在のテスト環境である JSDOM に別途 structuredClone の機能を追加する必要があります。

3. 対処法

このエラーを解消するには、前述の通りテスト環境に structuredClone の機能を追加すればよいです。これをおこなうための方法をいくつか紹介します。

選択肢① 追加の設定ファイルを用意する

テスト環境をカスタマイズするためのファイルを用意して、機能を強制的に追加する方法です。

https://github.com/jsdom/jsdom/issues/3363#issuecomment-1467894943

まず test ディレクトリ内に新しく次の内容のファイル FixJSDOMEnvironment.ts を作成します。
(ルートディレクトリに配置してもよいですが、私の場合は ESLint と併用したときに設定が狂ったので test ディレクトリに配置しています。)

FixJSDOMEnvironment.ts
import JSDOMEnvironment from 'jest-environment-jsdom';

export default class FixJSDOMEnvironment extends JSDOMEnvironment {
    constructor(...args: ConstructorParameters<typeof JSDOMEnvironment>) {
        super(...args);

        this.global.structuredClone = structuredClone;
    }
}

import JSDOMEnvironment from 'jest-environment-jsdom' で従来の JSDOM 環境をインポートし、それを継承した新しいクラス FixJSDOMEnvironment を定義しています。
コンストラクターで super() を呼び出した後に this.global.structuredClone = structuredClone とすることで、テスト環境にグローバルな関数として structuredClone を追加しています。

次に jest.config.jstestEnvironment を追加したファイルのパスに変更します。

jest.config.js
/** @type {import('ts-jest').JestConfigWithTsJest} */
  module.exports = {
    preset: 'ts-jest',
-   testEnvironment: 'jsdom',
+   testEnvironment: './tests/FixJSDOMEnvironment.ts',
    roots: ['<rootDir>/tests'],
  };

これで、カスタマイズされた新しい環境 FixJSDOMEnvironment にテスト環境を変更することができました。

yarn jest で再度テストを実行すると、今度は structuredClone がテスト環境に正しく定義されているのでテストに成功します。

PS C:\...\JsdomOverrideTest> yarn jest
yarn run v1.22.22
$ C:\...\JsdomOverrideTest\node_modules\.bin\jest
 PASS  tests/script.test.ts
  テスト
    √ オブジェクトを複製する (4 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        3.664 s
Ran all test suites.
Done in 4.41s.

選択肢② jest-fixed-jsdom ライブラリを追加する

他の選択肢として、jest-fixed-jsdom という JSDOM に不足しているグローバルな機能を一通り追加してくれるライブラリを使う方法があります。
このライブラリの README には「このパッケージは何かを解決することを意図したものではなく、一時的に問題を回避するためのものだよ」とあります。
ライブラリの中身を見るとやっていることは 選択肢① とほぼ同じなので、どちらの方法を使っても差はないと思います。

まず yarn add -D jest-fixed-jsdom でライブラリを追加でインストールします。
インストールすると、package.jsonjest-fixed-jsdom の項目が追加されます。

package.json
  {
    "name": "jsdom-override-test",
    "license": "UNLICENSED",
    "devDependencies": {
      "@types/jest": "^30.0.0",
      "jest": "^30.0.4",
      "jest-environment-jsdom": "^30.0.4",
+     "jest-fixed-jsdom": "^0.0.9",
      "ts-jest": "^29.4.0",
      "typescript": "^5.8.3"
    }
  }

次に jest.config.jstestEnvironmentjest-fixed-jsdom に変更します。

jest.config.js
/** @type {import('ts-jest').JestConfigWithTsJest} */
  module.exports = {
    preset: 'ts-jest',
-   testEnvironment: 'jsdom',
+   testEnvironment: 'jest-fixed-jsdom',
    roots: ['<rootDir>/tests'],
  };

これで再度 yarn jest でテストを実行すると、選択肢① と同様にテストに成功します。

PS C:\...\JsdomOverrideTest> yarn jest
yarn run v1.22.22
$ C:\...\JsdomOverrideTest\node_modules\.bin\jest
 PASS  tests/script.test.ts
  テスト
    √ オブジェクトを複製する (2 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        2.175 s, estimated 4 s
Ran all test suites.
Done in 2.76s.

選択肢③(※非推奨) JSON を使ってテストコードに追加する

JSON.parse(JSON.stringify(value)) を使ってテストコードに直接 structuredClone のダミーを追加する方法です。
しかし JSON.parse(JSON.stringify(value)) は本来の structuredClone 関数の下位互換であり、意図しない挙動を引き起こしかねないので推奨しません。

script.test.ts (一部)
  import { copyObject } from '../src/script';

+ global.structuredClone = (value: any) => {
+     return JSON.parse(JSON.stringify(value));
+ };

  ...

終わりに

以上、Jest を使ってテストをする際に ReferenceError: structuredClone is not defined が出たときの原因と対処法を解説しました。
テスト環境を追加のファイルを作ってカスタマイズできるというのは初めて知りました。
もし同じようなエラーに遭遇した場合は、本記事に載っている方法を試してみてください。

脚注
  1. 構造化複製アルゴリズム - Web API | MDN - https://developer.mozilla.org/ja/docs/Web/API/Web_Workers_API/Structured_clone_algorithm#対応済みの型 ↩︎

  2. jest-fixed-jsdom/README.md at main · mswjs/jest-fixed-jsdom - https://github.com/mswjs/jest-fixed-jsdom/blob/main/README.md ↩︎

Discussion