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.json
と tsconfig.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"
}
}
{
"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
に次のコードを書きます。
/** @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 でテストすることにします。
export function copyObject<T>(original: T): T {
return structuredClone(original);
}
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-jsdom
が structuredClone
に対応していない からです。
JSDOM ではブラウザと Node.js の両方で古いバージョンから利用できた機能には対応しているものの、Request
・TextEncoder
・structuredClone
など一部の (比較的最近追加された?) グローバル変数や Web API には対応しておらず、これらの機能を利用しようとするとエラーが発生するようです[2]。
そのため、現在のテスト環境である JSDOM に別途 structuredClone
の機能を追加する必要があります。
3. 対処法
このエラーを解消するには、前述の通りテスト環境に structuredClone
の機能を追加すればよいです。これをおこなうための方法をいくつか紹介します。
選択肢① 追加の設定ファイルを用意する
テスト環境をカスタマイズするためのファイルを用意して、機能を強制的に追加する方法です。
まず test
ディレクトリ内に新しく次の内容のファイル FixJSDOMEnvironment.ts
を作成します。
(ルートディレクトリに配置してもよいですが、私の場合は ESLint と併用したときに設定が狂ったので test
ディレクトリに配置しています。)
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.js
の testEnvironment
を追加したファイルのパスに変更します。
/** @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.json
に jest-fixed-jsdom
の項目が追加されます。
{
"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.js
の testEnvironment
を jest-fixed-jsdom
に変更します。
/** @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
関数の下位互換であり、意図しない挙動を引き起こしかねないので推奨しません。
import { copyObject } from '../src/script';
+ global.structuredClone = (value: any) => {
+ return JSON.parse(JSON.stringify(value));
+ };
...
終わりに
以上、Jest を使ってテストをする際に ReferenceError: structuredClone is not defined
が出たときの原因と対処法を解説しました。
テスト環境を追加のファイルを作ってカスタマイズできるというのは初めて知りました。
もし同じようなエラーに遭遇した場合は、本記事に載っている方法を試してみてください。
-
構造化複製アルゴリズム - Web API | MDN - https://developer.mozilla.org/ja/docs/Web/API/Web_Workers_API/Structured_clone_algorithm#対応済みの型 ↩︎
-
jest-fixed-jsdom/README.md at main · mswjs/jest-fixed-jsdom - https://github.com/mswjs/jest-fixed-jsdom/blob/main/README.md ↩︎
Discussion