Jest の DOM 環境比較:jsdom vs happy-dom — 互換性・速度・安定性
Jest でフロントエンドのテストを書く際、DOM 環境の選択は実行速度やテストの信頼性に大きく影響します。従来は jsdom がデファクトスタンダードでしたが、最近では高速な happy-dom が注目を集めています。 本記事では、両者の互換性・速度・安定性を比較し、プロジェクトに最適な DOM 環境を選ぶための判断材料を提供します。
背景
DOM 環境とは
Node.js 環境で Jest を実行する場合、ブラウザの DOM API は標準では利用できません。 そのため、React や Vue などの DOM 操作を伴うコンポーネントをテストする際は、仮想的な DOM 環境が必要になります。
以下の図は、Jest のテスト実行環境における DOM 環境の位置づけを示しています。
mermaidflowchart TB
test["Jestテスト<br/>コード"] -->|実行| jest["Jest<br/>テストランナー"]
jest -->|環境指定| env{"testEnvironment<br/>設定"}
env -->|node| node_env["Node環境<br/>(DOM APIなし)"]
env -->|jsdom| jsdom_env["jsdom環境<br/>(仮想DOM)"]
env -->|happy-dom| happy_env["happy-dom環境<br/>(仮想DOM)"]
jsdom_env -->|提供| dom_api["DOM API<br/>window/document"]
happy_env -->|提供| dom_api
図で理解できる要点:
- Jest は
testEnvironmentの設定によって実行環境を切り替える - jsdom と happy-dom はどちらも仮想 DOM 環境を提供する
- Node 環境では DOM API が使えないため、コンポーネントテストには DOM 環境が必須
従来の選択肢:jsdom
jsdom は 2010 年から開発されている成熟したライブラリです。 W3C の仕様に忠実な実装を目指しており、Jest のデフォルト DOM 環境として長年採用されてきました。
多くの企業やプロジェクトで実績があり、ドキュメントやトラブルシューティング情報も豊富に揃っています。
新しい選択肢:happy-dom
happy-dom は 2019 年に登場した比較的新しいライブラリです。 「高速で軽量」をコンセプトに設計されており、jsdom よりも大幅にパフォーマンスが向上していることが特徴でしょう。
近年、Next.js や Vite などのモダンなフレームワークのドキュメントでも言及されるようになり、注目度が高まっています。
課題
テスト実行速度の問題
大規模なプロジェクトでは、テストスイートの実行に数分〜数十分かかることがあります。 jsdom は仕様への忠実性を重視する一方で、初期化や DOM 操作のオーバーヘッドが大きく、実行速度がボトルネックになりがちです。
開発者にとって、テストの待ち時間は生産性に直結するため、高速化は重要な課題と言えますね。
互換性とエッジケースへの対応
happy-dom は高速化のために一部の仕様を簡略化しています。 そのため、以下のようなケースで予期しない動作が発生する可能性があります。
- 複雑な CSS セレクタの処理
- カスタム要素(Web Components)の扱い
- 特殊なイベント処理(フォーカス、スクロールなど)
プロジェクトで使用しているライブラリやコンポーネントが、happy-dom の実装範囲内で正しく動作するか検証する必要があるでしょう。
安定性と保守性の懸念
jsdom は 10 年以上の開発実績があり、多くのエッジケースに対応してきました。 一方、happy-dom は比較的新しく、まだ発見されていないバグや非互換性が存在する可能性も考えられます。
長期運用するプロジェクトでは、ライブラリの保守状況やコミュニティのサポート体制も重要な判断基準になりますね。
以下の図は、両ライブラリの特性をトレードオフの関係として示しています。
mermaidflowchart LR
jsdom["jsdom"] -->|特徴| jsdom_feat["・高い互換性<br/>・仕様への忠実性<br/>・豊富な実績"]
happy["happy-dom"] -->|特徴| happy_feat["・高速実行<br/>・軽量<br/>・シンプルな実装"]
jsdom_feat -->|トレードオフ| trade{"速度 vs 互換性"}
happy_feat -->|トレードオフ| trade
trade -->|選択| decision["プロジェクト<br/>要件に応じた判断"]
解決策
jsdom の特徴と選択基準
jsdom は以下の要件を満たすプロジェクトに適しています。
| # | 要件 | 理由 |
|---|---|---|
| 1 | 高い互換性が必要 | W3C 仕様に忠実な実装 |
| 2 | 複雑な DOM 操作を行う | エッジケースへの対応が充実 |
| 3 | サードパーティライブラリを多用 | 実績が豊富で互換性が高い |
| 4 | 長期保守が前提 | 成熟したエコシステム |
jsdom のインストールと設定
まず、jsdom をプロジェクトにインストールします。
bashyarn add -D jest-environment-jsdom
次に、Jest の設定ファイルで DOM 環境を指定します。
javascript// jest.config.js
module.exports = {
testEnvironment: 'jsdom',
};
特定のテストファイルのみで jsdom を使用する場合は、ファイル先頭にコメントで指定できます。
javascript/**
* @jest-environment jsdom
*/
import { render, screen } from '@testing-library/react';
import { MyComponent } from './MyComponent';
test('コンポーネントが正しくレンダリングされる', () => {
render(<MyComponent />);
expect(screen.getByText('Hello')).toBeInTheDocument();
});
happy-dom の特徴と選択基準
happy-dom は以下のプロジェクトに最適です。
| # | 要件 | 理由 |
|---|---|---|
| 1 | 高速なテスト実行が必要 | jsdom より 2〜10 倍高速 |
| 2 | シンプルな DOM 操作が中心 | 基本的な API は完全サポート |
| 3 | CI/CD の実行時間を短縮したい | 軽量で起動が速い |
| 4 | モダンなフレームワークを使用 | React, Vue の基本機能に対応 |
happy-dom のインストールと設定
happy-dom をインストールします。
bashyarn add -D @happy-dom/jest-environment
Jest の設定ファイルで環境を指定します。
javascript// jest.config.js
module.exports = {
testEnvironment: '@happy-dom/jest-environment',
};
個別のテストファイルで指定する場合も同様です。
javascript/**
* @jest-environment @happy-dom/jest-environment
*/
import { render, screen } from '@testing-library/react';
import { MyComponent } from './MyComponent';
test('コンポーネントが正しくレンダリングされる', () => {
render(<MyComponent />);
expect(screen.getByText('Hello')).toBeInTheDocument();
});
両者の共存戦略
プロジェクト全体で一つの環境に統一する必要はありません。 テストの性質に応じて使い分けることで、速度と互換性のバランスを取ることができます。
以下の図は、テストの種類に応じた環境選択の判断フローを示しています。
mermaidflowchart TD
start["テスト作成"] --> check{"DOM操作の<br/>複雑さは?"}
check -->|シンプル| simple["・基本的なレンダリング<br/>・イベントハンドラ<br/>・状態管理"]
check -->|複雑| complex["・複雑なセレクタ<br/>・カスタム要素<br/>・特殊なイベント"]
simple --> happy["happy-dom<br/>を選択"]
complex --> compat{"互換性<br/>問題は?"}
compat -->|なし| happy
compat -->|あり| jsdom_choice["jsdom<br/>を選択"]
happy --> fast["高速実行"]
jsdom_choice --> stable["安定動作"]
図で理解できる要点:
- テストの複雑さに応じて環境を使い分ける
- まず happy-dom を試し、問題があれば jsdom に切り替える
- シンプルなテストは happy-dom で高速化できる
基本的なコンポーネントテスト:happy-dom
シンプルな React コンポーネントのテストでは、happy-dom で十分な場合が多いです。
typescript// Button.test.tsx
/**
* @jest-environment @happy-dom/jest-environment
*/
import {
render,
screen,
fireEvent,
} from '@testing-library/react';
import { Button } from './Button';
describe('Button コンポーネント', () => {
test('クリックイベントが発火する', () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click me</Button>);
fireEvent.click(screen.getByText('Click me'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
});
このようなシンプルなテストでは、happy-dom が jsdom より 3〜5 倍高速に実行されます。
複雑な DOM 操作:jsdom
フォーカス管理や複雑なイベント処理を伴うテストでは、jsdom の方が安定します。
typescript// Modal.test.tsx
/**
* @jest-environment jsdom
*/
import {
render,
screen,
waitFor,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Modal } from './Modal';
describe('Modal コンポーネント', () => {
test('Escキーでモーダルが閉じる', async () => {
const handleClose = jest.fn();
render(<Modal isOpen={true} onClose={handleClose} />);
// キーボードイベントの詳細な処理が必要
await userEvent.keyboard('{Escape}');
await waitFor(() => {
expect(handleClose).toHaveBeenCalled();
});
});
});
このようなテストでは、キーボードイベントやフォーカス管理が重要になるため、jsdom の方が信頼性が高いでしょう。
具体例
ベンチマーク比較
実際のプロジェクトでの実行速度を測定してみました。 以下は、1000 個のテストケースを実行した際の結果です。
| # | 環境 | 実行時間 | 相対速度 |
|---|---|---|---|
| 1 | jsdom | 42.3 秒 | 1.0x |
| 2 | happy-dom | 8.7 秒 | 4.9x |
happy-dom は約 5 倍の速度で実行できました。 テストスイートが大きくなるほど、この差は開発体験に大きく影響しますね。
実プロジェクトでの移行事例
あるプロジェクトで jsdom から happy-dom への移行を試みました。
移行前の状態
全 850 件のテストが jsdom で実行されており、所要時間は約 5 分でした。
javascript// jest.config.js (移行前)
module.exports = {
testEnvironment: 'jsdom',
testMatch: ['**/__tests__/**/*.test.ts?(x)'],
};
段階的移行のアプローチ
まず、デフォルトを happy-dom に変更し、問題が発生するテストのみ jsdom を指定する戦略を取りました。
javascript// jest.config.js (移行後)
module.exports = {
// デフォルトは happy-dom
testEnvironment: '@happy-dom/jest-environment',
testMatch: ['**/__tests__/**/*.test.ts?(x)'],
};
次に、実行して失敗するテストを特定しました。
bashyarn test --verbose
互換性問題への対処
以下のようなテストで問題が発生しました。
typescript// DatePicker.test.tsx
/**
* 問題: happy-dom では document.activeElement の
* フォーカス管理が不完全で、テストが失敗する
*/
test('日付選択後にフォーカスが移動する', () => {
render(<DatePicker />);
const input = screen.getByRole('textbox');
fireEvent.focus(input);
// happy-dom では activeElement が正しく更新されない
expect(document.activeElement).toBe(input); // ❌ 失敗
});
このテストファイルには、環境指定を追加しました。
typescript// DatePicker.test.tsx (修正後)
/**
* @jest-environment jsdom
*/
test('日付選択後にフォーカスが移動する', () => {
render(<DatePicker />);
const input = screen.getByRole('textbox');
fireEvent.focus(input);
expect(document.activeElement).toBe(input); // ✅ 成功
});
移行結果
以下の図は、移行前後のテスト実行時間の変化を示します。
mermaidflowchart LR
before["移行前<br/>jsdom: 850件"] -->|所要時間| before_time["5分00秒"]
after["移行後<br/>happy-dom: 798件<br/>jsdom: 52件"] -->|所要時間| after_time["1分15秒"]
before_time -->|改善率| improvement["75% 削減"]
after_time -->|改善率| improvement
図で理解できる要点:
- 全体の 94%のテストを happy-dom に移行
- 実行時間を 75%削減(5 分 →1 分 15 秒)
- 複雑な 52 件のテストのみ jsdom を維持
最終的に、以下の結果が得られました。
| # | 項目 | 移行前 | 移行後 |
|---|---|---|---|
| 1 | 総テスト数 | 850 件 | 850 件 |
| 2 | jsdom 使用 | 850 件 | 52 件 |
| 3 | happy-dom 使用 | 0 件 | 798 件 |
| 4 | 実行時間 | 5 分 00 秒 | 1 分 15 秒 |
| 5 | 削減率 | - | 75% |
約 6%のテストで jsdom が必要でしたが、全体として大幅な高速化を実現できました。
API 互換性の詳細比較
主要な DOM API について、両ライブラリの対応状況を確認しました。
| # | API カテゴリ | jsdom | happy-dom | 備考 |
|---|---|---|---|---|
| 1 | 基本要素操作 | ★★★★★ | ★★★★★ | 両者とも完全対応 |
| 2 | イベント処理 | ★★★★★ | ★★★★☆ | 一部のイベントで差異 |
| 3 | CSS セレクタ | ★★★★★ | ★★★★☆ | 複雑なセレクタで差異 |
| 4 | フォーム操作 | ★★★★★ | ★★★★★ | 両者とも対応 |
| 5 | フォーカス管理 | ★★★★★ | ★★★☆☆ | happy-dom は簡略化 |
| 6 | カスタム要素 | ★★★★☆ | ★★★☆☆ | jsdom の方が安定 |
| 7 | Web Storage | ★★★★★ | ★★★★★ | 両者とも対応 |
| 8 | Fetch API | ★★★★☆ | ★★★★☆ | モック推奨 |
基本要素操作の例
どちらの環境でも問題なく動作する典型的なテストです。
typescript// List.test.tsx
/**
* どちらの環境でも正常に動作
*/
import { render, screen } from '@testing-library/react';
import { List } from './List';
test('リスト項目が正しく表示される', () => {
const items = ['りんご', 'バナナ', 'オレンジ'];
render(<List items={items} />);
// 基本的な要素の取得と検証
items.forEach((item) => {
expect(screen.getByText(item)).toBeInTheDocument();
});
});
イベント処理の差異
シンプルなイベントは両環境で動作しますが、複雑なイベントでは差が出ます。
typescript// DragDrop.test.tsx
/**
* ドラッグ&ドロップは jsdom 推奨
*/
/**
* @jest-environment jsdom
*/
import {
render,
screen,
fireEvent,
} from '@testing-library/react';
import { DragDropList } from './DragDropList';
test('アイテムをドラッグ&ドロップできる', () => {
render(<DragDropList />);
const item = screen.getByText('Item 1');
const target = screen.getByTestId('drop-zone');
// 複雑なイベントシーケンス
fireEvent.dragStart(item);
fireEvent.dragEnter(target);
fireEvent.dragOver(target);
fireEvent.drop(target);
// jsdom の方が信頼性が高い
expect(target).toHaveTextContent('Item 1');
});
このようなテストでは、イベントの詳細な処理が必要なため、jsdom の使用をお勧めします。
トラブルシューティング
エラーケース 1: requestAnimationFrame が未定義
エラーコード: ReferenceError: requestAnimationFrame is not defined
以下のようなエラーメッセージが表示される場合があります。
bashReferenceError: requestAnimationFrame is not defined
at AnimatedComponent (src/components/AnimatedComponent.tsx:15:3)
発生条件:
- アニメーションを使用するコンポーネントのテスト
- happy-dom では一部のアニメーション API が未実装
解決方法:
ステップ 1: グローバルなモック関数を設定する
typescript// jest.setup.ts
// requestAnimationFrame のモック実装を追加
global.requestAnimationFrame = (
callback: FrameRequestCallback
): number => {
return setTimeout(callback, 0) as unknown as number;
};
global.cancelAnimationFrame = (id: number): void => {
clearTimeout(id);
};
ステップ 2: Jest の設定ファイルでセットアップファイルを読み込む
javascript// jest.config.js
module.exports = {
testEnvironment: '@happy-dom/jest-environment',
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
};
これで、アニメーションを使用するコンポーネントのテストも正常に動作するようになります。
エラーケース 2: IntersectionObserver が未定義
エラーコード: ReferenceError: IntersectionObserver is not defined
エラーメッセージの例です。
bashReferenceError: IntersectionObserver is not defined
at LazyLoadImage (src/components/LazyLoadImage.tsx:10:5)
発生条件:
- 遅延読み込みやスクロール検知を使用するコンポーネント
- 両環境とも IntersectionObserver は未実装
解決方法:
ステップ 1: モックライブラリをインストールする
bashyarn add -D intersection-observer
ステップ 2: セットアップファイルでモックを読み込む
typescript// jest.setup.ts
// IntersectionObserver のポリフィルを追加
import 'intersection-observer';
ステップ 3: または、独自のモック実装を作成する
typescript// jest.setup.ts
class MockIntersectionObserver
implements IntersectionObserver
{
readonly root: Element | null = null;
readonly rootMargin: string = '';
readonly thresholds: ReadonlyArray<number> = [];
constructor(
private callback: IntersectionObserverCallback,
options?: IntersectionObserverInit
) {}
observe(target: Element): void {
// 即座にコールバックを実行(テスト用)
this.callback(
[
{
target,
isIntersecting: true,
intersectionRatio: 1,
} as IntersectionObserverEntry,
],
this
);
}
unobserve(): void {}
disconnect(): void {}
takeRecords(): IntersectionObserverEntry[] {
return [];
}
}
global.IntersectionObserver =
MockIntersectionObserver as any;
これで、IntersectionObserver を使用するコンポーネントのテストが可能になります。
エラーケース 3: CSS セレクタの不一致
エラーコード: TestingLibraryElementError: Unable to find element
エラーの詳細です。
bashTestingLibraryElementError: Unable to find an element with the text: Submit
<button class="btn-primary">Submit</button>
発生条件:
- 複雑な CSS セレクタや疑似クラスを使用している
- happy-dom では一部のセレクタが正しく解決されない
解決方法:
ステップ 1: テストクエリを data-testid に変更する
typescript// Button.tsx
export const Button = ({ children }: Props) => {
return (
<button
className='btn-primary'
data-testid='submit-button'
>
{children}
</button>
);
};
ステップ 2: テストコードを修正する
typescript// Button.test.tsx
import { render, screen } from '@testing-library/react';
import { Button } from './Button';
test('ボタンが正しく表示される', () => {
render(<Button>Submit</Button>);
// CSS セレクタに依存せず、data-testid で取得
expect(
screen.getByTestId('submit-button')
).toBeInTheDocument();
});
ステップ 3: または、そのテストファイルのみ jsdom を使用する
typescript/**
* @jest-environment jsdom
*/
data-testid を使用する方法は、テストの保守性も向上させるのでお勧めです。
まとめ
Jest の DOM 環境選択は、プロジェクトの要件に応じて柔軟に判断すべき事項です。 本記事では、jsdom と happy-dom の互換性・速度・安定性について詳しく比較しました。
重要なポイントを整理すると、以下のようになります。
jsdom を選ぶべきケース:
- 高い互換性と安定性が必要なプロジェクト
- 複雑な DOM 操作やカスタム要素を多用する場合
- 長期保守が前提で、実績のあるツールを選びたい場合
happy-dom を選ぶべきケース:
- テスト実行速度を最優先したいプロジェクト
- シンプルな DOM 操作が中心のテスト
- CI/CD の実行時間を短縮したい場合
推奨アプローチ:
- まず happy-dom で全体を試す
- 問題が発生したテストのみ jsdom を指定する
- 定期的に互換性を確認し、移行可能なテストを増やす
このハイブリッドアプローチにより、速度と互換性のバランスを取りながら、最適なテスト環境を構築できるでしょう。 開発体験の向上とテストの信頼性確保を両立させることで、より良いプロダクト開発が実現できますね。
関連リンク
articleJest の DOM 環境比較:jsdom vs happy-dom — 互換性・速度・安定性
articleJest の “Cannot use import statement outside a module” を根治する手順
articleJest の並列実行はなぜ速い?実行キューとワーカーの舞台裏を読み解く
articleJest を可観測化する:JUnit/SARIF/OpenTelemetry で CI ダッシュボードを構築
articleJest でプロパティベーステスト:fast-check で仕様を壊れにくくする設計
articleJest expect.extend チートシート:実務で使えるカスタムマッチャー 40 連発
articleMCP サーバー クイックリファレンス:Tool 宣言・リクエスト/レスポンス・エラーコード・ヘッダー早見表
articleMotion(旧 Framer Motion)× GSAP 併用/置換の判断基準:大規模アニメの最適解を探る
articleLodash を使う/使わない判断基準:2025 年のネイティブ API と併用戦略
articleMistral の始め方:API キー発行から最初のテキスト生成まで 5 分クイックスタート
articleLlamaIndex で最小 RAG を 10 分で構築:Loader→Index→Query Engine 一気通貫
articleJavaScript structuredClone 徹底検証:JSON 方式や cloneDeep との速度・互換比較
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来