Jest で DOM 操作をテストする方法:document・window の扱い方まとめ

フロントエンド開発において、DOM 操作のテストは避けて通れない重要な要素です。しかし、Node.js 環境で動作する Jest では、ブラウザの document オブジェクトや window オブジェクトが標準で利用できません。
この記事では、Jest を使って DOM 操作を効果的にテストする方法を段階的に解説します。設定から実践的なテストケースまで、具体的なコード例を交えながら、初心者の方でも理解できるよう丁寧に説明していきます。
DOM 操作テストでよく遭遇する「document is not defined」「window is not defined」といったエラーの解決方法も網羅的にカバーしており、実際の開発現場ですぐに活用できる内容となっています。
背景
フロントエンド開発におけるテストの重要性
現代のフロントエンド開発では、複雑な UI インタラクションやデータの動的な表示が求められます。ユーザーのクリック操作によってフォームが表示されたり、API からのデータ取得後に DOM 要素が動的に生成されたりするケースが日常的に発生しています。
こうした DOM 操作が正確に動作することを保証するためには、テストが不可欠です。テストを書くことで、リファクタリング時の回帰バグを防げるだけでなく、機能追加時の既存機能への影響も事前に検出できます。
特に、チーム開発において DOM 操作テストは重要な役割を果たします。担当者が変わっても、テストケースを見ることで期待される動作を理解でき、安心してコードを変更できる環境が整います。
Jest と DOM 操作テストの関係性
Jest は Facebook(現 Meta)が開発した JavaScript テストフレームワークです。設定が簡単で、スナップショットテストやモック機能が充実しているため、React をはじめとする多くのフロントエンドプロジェクトで採用されています。
しかし、Jest は Node.js 環境で動作するため、ブラウザ固有の API である DOM オブジェクトは標準では利用できません。これが DOM 操作テストを困難にしている主な要因です。
Jest の強力な機能を活用しつつ、DOM 操作をテストするためには、適切な環境設定と仮想 DOM の導入が必要になります。この関係性を理解することで、効果的なテスト戦略を立てることができるでしょう。
jsdom の役割と必要性
jsdom は、純粋な JavaScript で実装された DOM と HTML の実装です。Node.js 環境でブラウザの DOM API を再現することで、サーバーサイドでも DOM 操作をテストできるようになります。
Jest では、jsdom を testEnvironment として設定することで、テスト実行時に仮想的なブラウザ環境を構築できます。これにより、document オブジェクトや window オブジェクトが利用可能になり、DOM 操作のテストが実現します。
jsdom を使用することで、実際のブラウザを起動することなく、高速にテストを実行できる点も大きなメリットです。CI/CD 環境でのテスト実行も容易になり、開発効率の向上に大きく寄与します。
課題
Node.js 環境での DOM 操作テストの困難さ
Node.js は本来サーバーサイド JavaScript の実行環境として設計されており、ブラウザ固有の API は含まれていません。そのため、フロントエンド開発で当たり前のように使用している document や window オブジェクトは存在しません。
以下の図は、Node.js 環境とブラウザ環境の違いを示しています。
mermaidflowchart TB
subgraph Browser["ブラウザ環境"]
DOM[DOM API]
Window[Window API]
Storage[Storage API]
end
subgraph NodeJS["Node.js 環境"]
FS[File System]
HTTP[HTTP Module]
Process[Process API]
end
Jest --> NodeJS
Frontend --> Browser
この環境の違いにより、フロントエンドコードをそのまま Node.js 環境でテストしようとすると、様々なエラーが発生します。特に DOM 要素の操作や、ブラウザイベントの処理をテストする際に大きな障壁となります。
document・window オブジェクトの未定義問題
最も頻繁に遭遇するのが、以下のようなエラーメッセージです。
javascript// エラー例:ReferenceError: document is not defined
const element = document.getElementById('myButton');
// エラー例:ReferenceError: window is not defined
const width = window.innerWidth;
これらのエラーは、Node.js 環境にはブラウザの DOM API が存在しないために発生します。通常のフロントエンド開発では自然に使用できるこれらのオブジェクトが、テスト環境では利用できないのです。
エラーパターン | 発生箇所 | 影響範囲 |
---|---|---|
document is not defined | DOM 要素の取得・操作 | 要素の検索、スタイル変更、内容更新 |
window is not defined | ブラウザ API の呼び出し | サイズ取得、イベント処理、ナビゲーション |
localStorage is not defined | ストレージ操作 | データの保存・取得、セッション管理 |
ブラウザ環境とテスト環境の差異
ブラウザとテスト環境では、DOM の実装や動作に細かな違いがあります。例えば、レンダリングエンジンの違いにより、計算されたスタイルの値や、イベントの発火タイミングが異なることがあります。
さらに、非同期処理の扱いも大きな課題です。ブラウザでは requestAnimationFrame や IntersectionObserver といった API が非同期で動作しますが、テスト環境ではこれらの API の動作を適切に再現し、テストする必要があります。
こうした環境差異を理解し、適切に対処することが、信頼性の高い DOM 操作テストを作成する鍵となります。
解決策
Jest の testEnvironment 設定
Jest で DOM 操作をテストするための最初のステップは、適切な testEnvironment を設定することです。デフォルトの Jest は Node.js 環境で動作しますが、jsdom 環境に切り替えることで DOM API が利用可能になります。
jest.config.js での設定方法
プロジェクトのルートディレクトリに jest.config.js ファイルを作成し、以下のように設定します。
javascriptmodule.exports = {
// jsdom 環境を指定
testEnvironment: 'jsdom',
// セットアップファイルを指定
setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
// テストファイルの場所を指定
testMatch: [
'<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}',
'<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}',
],
};
package.json での設定方法
シンプルな設定の場合は、package.json に直接記述することも可能です。
json{
"name": "my-project",
"jest": {
"testEnvironment": "jsdom"
}
}
必要パッケージのインストール
jsdom 環境を使用するには、以下のパッケージが必要です。
bashyarn add -D jest-environment-jsdom
jsdom による仮想 DOM 環境の構築
jsdom が提供する仮想 DOM 環境は、実際のブラウザ DOM をかなり忠実に再現します。これにより、document や window オブジェクトを使った操作がテスト環境でも実行可能になります。
セットアップファイルの作成
src/setupTests.js ファイルを作成し、テスト環境の初期化を行います。
javascript// DOM 環境のポリフィルを追加
import 'whatwg-fetch';
// カスタムマッチャーを追加
import '@testing-library/jest-dom';
// Global オブジェクトの設定
global.ResizeObserver = jest
.fn()
.mockImplementation(() => ({
observe: jest.fn(),
unobserve: jest.fn(),
disconnect: jest.fn(),
}));
jsdom の詳細設定
より細かい制御が必要な場合は、jsdom のオプションを指定できます。
javascriptmodule.exports = {
testEnvironment: 'jsdom',
testEnvironmentOptions: {
url: 'http://localhost:3000',
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
},
};
global オブジェクトの設定方法
一部のブラウザ API は jsdom でも完全には再現されていません。そのような場合は、手動でモックオブジェクトを設定する必要があります。
window オブジェクトへの API 追加
javascript// setupTests.js での設定例
global.window.matchMedia = jest
.fn()
.mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(),
removeListener: jest.fn(),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
}));
// IntersectionObserver のモック
global.IntersectionObserver = jest
.fn()
.mockImplementation(() => ({
observe: jest.fn(),
unobserve: jest.fn(),
disconnect: jest.fn(),
}));
localStorage・sessionStorage のモック
ストレージ API も手動で設定が必要な場合があります。
javascript// ストレージのモック実装
const localStorageMock = {
getItem: jest.fn(),
setItem: jest.fn(),
removeItem: jest.fn(),
clear: jest.fn(),
};
global.localStorage = localStorageMock;
global.sessionStorage = localStorageMock;
以下の表は、よく使用されるブラウザ API とその設定方法をまとめたものです。
API 名 | 設定の必要性 | 設定方法 |
---|---|---|
document | 不要 | jsdom で自動提供 |
window | 不要 | jsdom で自動提供 |
localStorage | 必要 | 手動モック |
matchMedia | 必要 | 手動モック |
IntersectionObserver | 必要 | 手動モック |
これらの設定により、Jest テスト環境でブラウザと同様の DOM 操作が可能になります。
具体例
document オブジェクトの基本的な操作テスト
document オブジェクトを使った基本的な DOM 操作をテストする方法から見ていきましょう。要素の取得、内容の変更、属性の操作など、実際の開発でよく使用される操作をテストできます。
要素の取得と内容変更のテスト
まず、HTML 要素を動的に生成し、その内容を変更する関数をテストしてみます。
javascript// src/utils/domUtils.js
export function updateElementText(id, newText) {
const element = document.getElementById(id);
if (element) {
element.textContent = newText;
return true;
}
return false;
}
export function createElement(tagName, attributes = {}) {
const element = document.createElement(tagName);
Object.keys(attributes).forEach((key) => {
element.setAttribute(key, attributes[key]);
});
return element;
}
対応するテストコードは以下のようになります。
javascript// src/utils/__tests__/domUtils.test.js
import {
updateElementText,
createElement,
} from '../domUtils';
describe('DOM ユーティリティのテスト', () => {
beforeEach(() => {
// テスト前に DOM をクリーンアップ
document.body.innerHTML = '';
});
test('要素のテキスト内容を更新できる', () => {
// 事前準備:テスト用の HTML 要素を作成
document.body.innerHTML = `
<div id="testElement">元のテキスト</div>
`;
// 実行
const result = updateElementText(
'testElement',
'新しいテキスト'
);
// 検証
expect(result).toBe(true);
expect(
document.getElementById('testElement').textContent
).toBe('新しいテキスト');
});
});
属性操作のテスト
HTML 要素の属性を操作する関数のテストも重要です。
javascripttest('要素を動的に作成し属性を設定できる', () => {
// 実行
const element = createElement('button', {
class: 'btn btn-primary',
'data-testid': 'submit-button',
disabled: 'true',
});
// DOM に追加
document.body.appendChild(element);
// 検証
expect(element.tagName).toBe('BUTTON');
expect(element.className).toBe('btn btn-primary');
expect(element.getAttribute('data-testid')).toBe(
'submit-button'
);
expect(element.hasAttribute('disabled')).toBe(true);
});
window オブジェクトのイベント処理テスト
window オブジェクトを使用したイベント処理は、多くの Web アプリケーションで重要な機能です。リサイズイベント、スクロールイベント、ロードイベントなどをテストする方法を見ていきます。
ウィンドウリサイズのイベント処理
画面サイズに応じてレイアウトを変更する機能をテストしてみましょう。
javascript// src/components/ResponsiveHandler.js
export class ResponsiveHandler {
constructor() {
this.isMobile = false;
this.breakpoint = 768;
this.init();
}
init() {
this.checkScreenSize();
window.addEventListener(
'resize',
this.handleResize.bind(this)
);
}
handleResize() {
this.checkScreenSize();
}
checkScreenSize() {
this.isMobile = window.innerWidth < this.breakpoint;
this.updateLayout();
}
updateLayout() {
const container = document.querySelector('.container');
if (container) {
container.classList.toggle(
'mobile-layout',
this.isMobile
);
}
}
}
このクラスのテストコードは以下のようになります。
javascript// src/components/__tests__/ResponsiveHandler.test.js
import { ResponsiveHandler } from '../ResponsiveHandler';
describe('ResponsiveHandler のテスト', () => {
let handler;
beforeEach(() => {
// DOM をセットアップ
document.body.innerHTML =
'<div class="container"></div>';
// window.innerWidth をモック
Object.defineProperty(window, 'innerWidth', {
writable: true,
configurable: true,
value: 1024,
});
handler = new ResponsiveHandler();
});
test('デスクトップサイズでは mobile-layout クラスが付与されない', () => {
// 検証
expect(handler.isMobile).toBe(false);
expect(
document
.querySelector('.container')
.classList.contains('mobile-layout')
).toBe(false);
});
});
resize イベントの発火テスト
javascripttest('ウィンドウリサイズ時にレイアウトが更新される', () => {
const container = document.querySelector('.container');
// 画面サイズを変更
Object.defineProperty(window, 'innerWidth', {
value: 500,
});
// resize イベントを発火
const resizeEvent = new Event('resize');
window.dispatchEvent(resizeEvent);
// 検証
expect(handler.isMobile).toBe(true);
expect(
container.classList.contains('mobile-layout')
).toBe(true);
});
DOM 要素の生成・操作・削除のテスト
動的な DOM 操作は、現代の Web アプリケーションの核心部分です。要素の生成から削除まで、一連の流れをテストする方法を詳しく見ていきましょう。
動的リスト管理のテスト
Todo リストのような動的にアイテムを追加・削除する機能をテストしてみます。
javascript// src/components/TodoList.js
export class TodoList {
constructor(containerId) {
this.container = document.getElementById(containerId);
this.todos = [];
this.init();
}
init() {
this.render();
}
addTodo(text) {
const id = Date.now().toString();
const todo = {
id,
text,
completed: false,
};
this.todos.push(todo);
this.renderTodoItem(todo);
return id;
}
renderTodoItem(todo) {
const todoElement = document.createElement('div');
todoElement.className = 'todo-item';
todoElement.setAttribute('data-id', todo.id);
todoElement.innerHTML = `
<input type="checkbox" ${
todo.completed ? 'checked' : ''
}>
<span class="todo-text">${todo.text}</span>
<button class="delete-btn">削除</button>
`;
this.setupTodoEvents(todoElement, todo.id);
this.container.appendChild(todoElement);
}
}
要素生成のテスト
javascript// src/components/__tests__/TodoList.test.js
import { TodoList } from '../TodoList';
describe('TodoList のテスト', () => {
let todoList;
beforeEach(() => {
document.body.innerHTML =
'<div id="todo-container"></div>';
todoList = new TodoList('todo-container');
});
test('新しい TODO アイテムを追加できる', () => {
// 実行
const todoId = todoList.addTodo('テストアイテム');
// 検証
const todoElement = document.querySelector(
`[data-id="${todoId}"]`
);
expect(todoElement).not.toBeNull();
expect(
todoElement.querySelector('.todo-text').textContent
).toBe('テストアイテム');
// 必要な要素が含まれているかチェック
expect(
todoElement.querySelector('input[type="checkbox"]')
).not.toBeNull();
expect(
todoElement.querySelector('.delete-btn')
).not.toBeNull();
});
});
要素削除のテスト
javascripttest('TODO アイテムを削除できる', () => {
// 事前準備
const todoId = todoList.addTodo('削除テスト');
const todoElement = document.querySelector(
`[data-id="${todoId}"]`
);
// 削除実行
todoList.removeTodo(todoId);
// 検証
const deletedElement = document.querySelector(
`[data-id="${todoId}"]`
);
expect(deletedElement).toBeNull();
expect(
todoList.todos.find((todo) => todo.id === todoId)
).toBeUndefined();
});
localStorage・sessionStorage のテスト
Web Storage API のテストは、データの永続化機能をテストする上で重要です。ユーザーの設定や一時的なデータの保存・取得をテストする方法を見ていきましょう。
ストレージマネージャーのテスト
ユーザー設定を管理するクラスをテストしてみます。
javascript// src/utils/StorageManager.js
export class StorageManager {
static setItem(key, value) {
try {
localStorage.setItem(key, JSON.stringify(value));
return true;
} catch (error) {
console.error('Storage error:', error);
return false;
}
}
static getItem(key) {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : null;
} catch (error) {
console.error('Parse error:', error);
return null;
}
}
static removeItem(key) {
localStorage.removeItem(key);
}
}
ストレージのテストでは、各テスト前にストレージをクリアすることが重要です。
javascript// src/utils/__tests__/StorageManager.test.js
import { StorageManager } from '../StorageManager';
describe('StorageManager のテスト', () => {
beforeEach(() => {
// ストレージをクリア
localStorage.clear();
jest.clearAllMocks();
});
test('データを正常に保存・取得できる', () => {
const testData = { name: 'テストユーザー', age: 25 };
// データ保存
const saveResult = StorageManager.setItem(
'user',
testData
);
expect(saveResult).toBe(true);
// データ取得
const retrievedData = StorageManager.getItem('user');
expect(retrievedData).toEqual(testData);
});
test('存在しないキーに対しては null を返す', () => {
const result = StorageManager.getItem('nonexistent');
expect(result).toBeNull();
});
});
セッションストレージのテスト
一時的なデータ管理をテストする場合も同様の方法が使用できます。
javascripttest('セッション固有のデータを管理できる', () => {
// sessionStorage を使用するバージョン
sessionStorage.setItem(
'sessionData',
JSON.stringify({ temp: true })
);
const data = JSON.parse(
sessionStorage.getItem('sessionData')
);
expect(data.temp).toBe(true);
// セッション終了をシミュレート
sessionStorage.clear();
expect(sessionStorage.getItem('sessionData')).toBeNull();
});
以下の表は、各ストレージ API の特徴とテスト時の注意点をまとめたものです。
ストレージタイプ | 保存期間 | 容量制限 | テスト時の注意点 |
---|---|---|---|
localStorage | 永続的 | 約 5-10MB | beforeEach でのクリア必須 |
sessionStorage | セッション中 | 約 5-10MB | タブ間での分離をテスト |
IndexedDB | 永続的 | 大容量 | 非同期処理のテストが必要 |
これらの具体例を通じて、Jest を使った DOM 操作テストの基本パターンを理解できたでしょう。実際のプロジェクトでは、これらのパターンを組み合わせて、より複雑な機能をテストすることになります。
まとめ
Jest で DOM 操作をテストする方法について、基礎から応用まで段階的に解説してきました。Node.js 環境で DOM API を利用するためには、適切な設定と理解が必要ですが、一度環境を整えれば強力なテスト環境を構築できます。
重要なポイント
環境設定の重要性: testEnvironment: 'jsdom'
の設定により、DOM API が利用可能になります。この一行の設定が、DOM 操作テストの基盤となります。
段階的なアプローチ: 基本的な document・window オブジェクトの操作から始めて、イベント処理、動的 DOM 操作、ストレージ操作と段階的にテストスキルを向上させることが効果的です。
実践的なテストパターン: 実際の開発で頻繁に使用される DOM 操作パターンをテストすることで、バグの早期発見と品質向上を実現できます。
次のステップ
この記事で紹介した手法をベースに、以下のような発展的なテストにも挑戦してみてください。
- React Testing Library と組み合わせたコンポーネントテスト
- 非同期 DOM 操作のテスト(fetch API、Promise との組み合わせ)
- パフォーマンステスト(大量の DOM 要素生成時の動作確認)
- アクセシビリティテスト(ARIA 属性、キーボードナビゲーション)
DOM 操作テストをマスターすることで、より安心してフロントエンド開発を進められるようになります。継続的な学習と実践を通じて、テストスキルをさらに向上させていきましょう。
関連リンク
- article
Jest で DOM 操作をテストする方法:document・window の扱い方まとめ
- article
【入門】Jest 初心者が最初に知っておくべきテスト設計の基本原則
- article
Jest でテストを書き続けるコツと開発文化への定着方法
- article
テストフレームワーク(Jest・Mocha)と Node.js
- article
Emotion と Jest/Testing Library で UI テストを快適に
- article
Jest のバージョンアップ手順とトラブルシューティング
- article
Svelte と GraphQL:最速データ連携のススメ
- article
Lodash の throttle・debounce でパフォーマンス最適化
- article
LangChain で RAG 構築:Retriever・VectorStore の設計ベストプラクティス
- article
Storybook で学ぶコンポーネントテスト戦略
- article
状態遷移を明文化する:XState × Jotai の堅牢な非同期フロー設計
- article
Jest で DOM 操作をテストする方法:document・window の扱い方まとめ
- blog
iPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
- blog
Googleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
- blog
【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
- blog
Googleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
- blog
Pixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
- blog
フロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来