T-CREATOR

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

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 definedDOM 要素の取得・操作要素の検索、スタイル変更、内容更新
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-10MBbeforeEach でのクリア必須
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 操作テストをマスターすることで、より安心してフロントエンド開発を進められるようになります。継続的な学習と実践を通じて、テストスキルをさらに向上させていきましょう。

関連リンク