T-CREATOR

Jest で複数環境(Node/Browser)のテストを両立する

Jest で複数環境(Node/Browser)のテストを両立する

現代の Web 開発では、フロントエンドとバックエンドの両方を扱うことが当たり前になっています。そんな中で、Jest を使って Node.js 環境とブラウザ環境の両方でテストを実行したいと思ったことはありませんか?

実際にやってみると、環境の違いによって予期しないエラーが発生したり、同じテストが片方の環境では通るのに、もう片方では失敗したりする経験をされた方も多いでしょう。

この記事では、Jest で複数環境のテストを効率的に両立させる方法を、実際のエラー例と解決策を交えて詳しく解説していきます。開発の品質向上と効率化を目指す方にとって、きっと役立つ内容になるはずです。

複数環境テストの必要性

Node.js 環境とブラウザ環境の違い

Node.js とブラウザ環境は、同じ JavaScript でも実行環境が大きく異なります。この違いを理解することが、複数環境テストの第一歩です。

Node.js 環境の特徴:

  • ファイルシステムへのアクセスが可能
  • processBuffer__dirnameなどの Node.js 固有の API が利用可能
  • モジュールシステム(CommonJS、ES Modules)が完全にサポート

ブラウザ環境の特徴:

  • DOM 操作が可能
  • windowdocumentlocalStorageなどのブラウザ API が利用可能
  • セキュリティ制限により、ファイルシステムへの直接アクセスは不可

フロントエンド・バックエンド両方のテストニーズ

現代の開発では、フルスタック開発が主流となっています。そのため、以下のようなテストニーズが生まれます:

  • ユニバーサルコードのテスト:フロントエンドとバックエンドで共有するロジック
  • API 層のテスト:Express.js などのサーバーサイドフレームワーク
  • UI コンポーネントのテスト:React、Vue.js などのフロントエンドフレームワーク
  • 統合テスト:フロントエンドとバックエンドの連携

環境固有の機能と制約

各環境には固有の機能と制約があります。これらを理解することで、適切なテスト戦略を立てることができます。

Node.js 環境の制約:

javascript// Node.js環境ではDOMが存在しないため、このコードはエラーになります
const element = document.getElementById('test');
element.innerHTML = 'Hello World';

ブラウザ環境の制約:

javascript// ブラウザ環境ではfsモジュールが存在しないため、このコードはエラーになります
const fs = require('fs');
const data = fs.readFileSync('test.txt', 'utf8');

Jest の環境設定の基本

Jest の環境設定オプション

Jest では、testEnvironmentオプションを使ってテストの実行環境を指定できます。この設定が複数環境テストの核となります。

利用可能な環境オプション:

  • node:Node.js 環境(デフォルト)
  • jsdom:ブラウザ環境(DOM API をシミュレート)
  • jsdom-global:jsdom をグローバルに設定
  • node-jsdom:Node.js と jsdom の両方を利用

testEnvironment の役割

testEnvironmentは、テストファイルがどの環境で実行されるかを決定します。この設定により、利用可能な API やグローバルオブジェクトが変わります。

基本的な設定例:

javascript// jest.config.js
module.exports = {
  testEnvironment: 'node', // Node.js環境
  // または
  testEnvironment: 'jsdom', // ブラウザ環境
};

デフォルト設定の理解

Jest のデフォルト設定を理解することで、カスタマイズの必要性を判断できます。

デフォルト設定の確認方法:

javascript// jest.config.js
module.exports = {
  // デフォルト設定を確認
  verbose: true,
  testEnvironment: 'node', // デフォルトは'node'
};

Node.js 環境でのテスト設定

Node.js 環境の特徴

Node.js 環境では、サーバーサイドの機能をテストできます。ファイルシステムアクセス、プロセス管理、ネットワーク通信などが可能です。

Node.js 環境の基本設定:

javascript// jest.config.node.js
module.exports = {
  testEnvironment: 'node',
  testMatch: ['**/__tests__/**/*.node.test.js'],
  setupFilesAfterEnv: ['<rootDir>/jest.setup.node.js'],
};

ファイルシステムアクセスのテスト

Node.js 環境では、ファイルシステムへのアクセスをテストできます。これはバックエンド開発では重要な機能です。

ファイル読み込みのテスト例:

javascript// fileUtils.node.test.js
const fs = require('fs');
const path = require('path');

describe('ファイル操作のテスト', () => {
  const testFilePath = path.join(__dirname, 'test.txt');

  beforeEach(() => {
    // テスト用ファイルを作成
    fs.writeFileSync(testFilePath, 'Hello World');
  });

  afterEach(() => {
    // テスト用ファイルを削除
    if (fs.existsSync(testFilePath)) {
      fs.unlinkSync(testFilePath);
    }
  });

  test('ファイルの読み込みが正常に動作する', () => {
    const content = fs.readFileSync(testFilePath, 'utf8');
    expect(content).toBe('Hello World');
  });
});

サーバーサイド機能のテスト

Express.js などのサーバーサイドフレームワークのテストも、Node.js 環境で実行できます。

Express.js アプリケーションのテスト例:

javascript// server.node.test.js
const request = require('supertest');
const app = require('../server');

describe('APIエンドポイントのテスト', () => {
  test('GET /api/users が正常にレスポンスを返す', async () => {
    const response = await request(app)
      .get('/api/users')
      .expect(200);

    expect(response.body).toHaveProperty('users');
    expect(Array.isArray(response.body.users)).toBe(true);
  });

  test('POST /api/users が新しいユーザーを作成する', async () => {
    const newUser = {
      name: 'Test User',
      email: 'test@example.com',
    };

    const response = await request(app)
      .post('/api/users')
      .send(newUser)
      .expect(201);

    expect(response.body).toHaveProperty('id');
    expect(response.body.name).toBe(newUser.name);
  });
});

ブラウザ環境でのテスト設定

jsdom 環境の設定

jsdom は、ブラウザ環境を Node.js でシミュレートするライブラリです。DOM 操作やブラウザ API のテストが可能になります。

jsdom 環境の基本設定:

javascript// jest.config.browser.js
module.exports = {
  testEnvironment: 'jsdom',
  testMatch: ['**/__tests__/**/*.browser.test.js'],
  setupFilesAfterEnv: ['<rootDir>/jest.setup.browser.js'],
  moduleNameMapping: {
    '\\.(css|less|scss|sass)$': 'identity-obj-proxy',
  },
};

DOM 操作のテスト

jsdom 環境では、DOM 操作をテストできます。これはフロントエンド開発では必須の機能です。

DOM 操作のテスト例:

javascript// domUtils.browser.test.js
describe('DOM操作のテスト', () => {
  let container;

  beforeEach(() => {
    // テスト用のDOM要素を作成
    container = document.createElement('div');
    container.id = 'test-container';
    document.body.appendChild(container);
  });

  afterEach(() => {
    // テスト用のDOM要素を削除
    if (container.parentNode) {
      container.parentNode.removeChild(container);
    }
  });

  test('要素の作成と追加が正常に動作する', () => {
    const element = document.createElement('p');
    element.textContent = 'Hello World';
    element.className = 'test-class';

    container.appendChild(element);

    expect(container.querySelector('p')).toBe(element);
    expect(element.textContent).toBe('Hello World');
    expect(element.className).toBe('test-class');
  });
});

ブラウザ API のモック

ブラウザ環境では、localStoragesessionStoragefetchなどのブラウザ API をモックする必要があります。

ブラウザ API のモック例:

javascript// browserAPI.browser.test.js
describe('ブラウザAPIのテスト', () => {
  beforeEach(() => {
    // localStorageのモック
    Object.defineProperty(window, 'localStorage', {
      value: {
        getItem: jest.fn(),
        setItem: jest.fn(),
        removeItem: jest.fn(),
        clear: jest.fn(),
      },
      writable: true,
    });

    // fetchのモック
    global.fetch = jest.fn();
  });

  test('localStorageの操作が正常に動作する', () => {
    const storage = window.localStorage;

    storage.setItem('test-key', 'test-value');
    storage.getItem('test-key');

    expect(storage.setItem).toHaveBeenCalledWith(
      'test-key',
      'test-value'
    );
    expect(storage.getItem).toHaveBeenCalledWith(
      'test-key'
    );
  });

  test('fetch APIの呼び出しが正常に動作する', async () => {
    const mockResponse = {
      json: () => Promise.resolve({ data: 'test' }),
    };
    global.fetch.mockResolvedValue(mockResponse);

    const response = await fetch('/api/test');
    const data = await response.json();

    expect(fetch).toHaveBeenCalledWith('/api/test');
    expect(data).toEqual({ data: 'test' });
  });
});

複数環境でのテスト実行戦略

環境別テストファイルの分離

複数環境でテストを実行する場合、テストファイルを環境別に分離することで、管理が容易になります。

推奨するディレクトリ構造:

vbnetsrc/
├── __tests__/
│   ├── node/
│   │   ├── fileUtils.node.test.js
│   │   └── server.node.test.js
│   ├── browser/
│   │   ├── domUtils.browser.test.js
│   │   └── browserAPI.browser.test.js
│   └── shared/
│       └── utils.shared.test.js

環境別の Jest 設定ファイル:

javascript// jest.config.node.js
module.exports = {
  testEnvironment: 'node',
  testMatch: ['**/__tests__/node/**/*.test.js'],
  setupFilesAfterEnv: ['<rootDir>/jest.setup.node.js'],
};

// jest.config.browser.js
module.exports = {
  testEnvironment: 'jsdom',
  testMatch: ['**/__tests__/browser/**/*.test.js'],
  setupFilesAfterEnv: ['<rootDir>/jest.setup.browser.js'],
};

共通テストと環境固有テストの使い分け

一部のテストは両方の環境で実行可能ですが、環境固有のテストは適切に分離する必要があります。

共通テストの例:

javascript// utils.shared.test.js
// このテストは両方の環境で実行可能
describe('ユーティリティ関数のテスト', () => {
  test('文字列の反転が正常に動作する', () => {
    const reverseString = (str) =>
      str.split('').reverse().join('');

    expect(reverseString('hello')).toBe('olleh');
    expect(reverseString('')).toBe('');
    expect(reverseString('a')).toBe('a');
  });

  test('配列の合計計算が正常に動作する', () => {
    const sumArray = (arr) =>
      arr.reduce((sum, num) => sum + num, 0);

    expect(sumArray([1, 2, 3, 4, 5])).toBe(15);
    expect(sumArray([])).toBe(0);
    expect(sumArray([-1, -2, 3])).toBe(0);
  });
});

テスト実行の自動化

package.json のスクリプトを使って、環境別のテスト実行を自動化できます。

package.json の設定例:

json{
  "scripts": {
    "test": "jest",
    "test:node": "jest --config jest.config.node.js",
    "test:browser": "jest --config jest.config.browser.js",
    "test:all": "yarn test:node && yarn test:browser",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage"
  }
}

実践的な設定例

package.json での設定

package.json で Jest の基本設定を行い、環境別の設定ファイルを参照する方法です。

package.json の設定:

json{
  "name": "multi-environment-jest",
  "version": "1.0.0",
  "scripts": {
    "test": "jest",
    "test:node": "jest --config jest.config.node.js",
    "test:browser": "jest --config jest.config.browser.js",
    "test:all": "yarn test:node && yarn test:browser"
  },
  "devDependencies": {
    "jest": "^29.0.0",
    "jest-environment-jsdom": "^29.0.0",
    "supertest": "^6.0.0"
  },
  "jest": {
    "collectCoverageFrom": [
      "src/**/*.{js,jsx,ts,tsx}",
      "!src/**/*.d.ts"
    ],
    "coverageThreshold": {
      "global": {
        "branches": 80,
        "functions": 80,
        "lines": 80,
        "statements": 80
      }
    }
  }
}

Jest 設定ファイルの活用

環境別の Jest 設定ファイルを作成することで、より細かい制御が可能になります。

メインの Jest 設定ファイル:

javascript// jest.config.js
module.exports = {
  // 共通設定
  verbose: true,
  collectCoverage: true,
  coverageDirectory: 'coverage',
  coverageReporters: ['text', 'lcov', 'html'],

  // テストファイルのパターン
  testMatch: [
    '**/__tests__/**/*.test.js',
    '**/?(*.)+(spec|test).js',
  ],

  // モジュール解決の設定
  moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx', 'json'],

  // 変換設定
  transform: {
    '^.+\\.(js|jsx|ts|tsx)$': 'babel-jest',
  },

  // モジュール名マッピング
  moduleNameMapping: {
    '\\.(css|less|scss|sass)$': 'identity-obj-proxy',
    '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
      '<rootDir>/__mocks__/fileMock.js',
  },
};

環境変数の活用

環境変数を使って、テスト環境を動的に制御することができます。

環境変数を使った設定例:

javascript// jest.config.dynamic.js
module.exports = {
  testEnvironment:
    process.env.TEST_ENV === 'browser' ? 'jsdom' : 'node',
  testMatch:
    process.env.TEST_ENV === 'browser'
      ? ['**/__tests__/browser/**/*.test.js']
      : ['**/__tests__/node/**/*.test.js'],
  setupFilesAfterEnv:
    process.env.TEST_ENV === 'browser'
      ? ['<rootDir>/jest.setup.browser.js']
      : ['<rootDir>/jest.setup.node.js'],
};

環境変数を使ったテスト実行:

bash# Node.js環境のテスト
TEST_ENV=node yarn test

# ブラウザ環境のテスト
TEST_ENV=browser yarn test

よくある問題と解決策

環境固有のエラー

複数環境でテストを実行する際に、よく遭遇するエラーとその解決策を紹介します。

エラー 1: document is not defined

javascript// エラーが発生するコード
test('DOM操作のテスト', () => {
  const element = document.getElementById('test'); // Node.js環境ではエラー
  expect(element).toBeTruthy();
});

// 解決策: 環境チェックを追加
test('DOM操作のテスト', () => {
  if (typeof document !== 'undefined') {
    const element = document.getElementById('test');
    expect(element).toBeTruthy();
  } else {
    // Node.js環境ではスキップ
    expect(true).toBe(true);
  }
});

エラー 2: fs is not defined

javascript// エラーが発生するコード
test('ファイル操作のテスト', () => {
  const fs = require('fs'); // ブラウザ環境ではエラー
  const content = fs.readFileSync('test.txt', 'utf8');
  expect(content).toBe('Hello World');
});

// 解決策: 動的インポートを使用
test('ファイル操作のテスト', async () => {
  if (typeof require !== 'undefined') {
    const fs = require('fs');
    const content = fs.readFileSync('test.txt', 'utf8');
    expect(content).toBe('Hello World');
  } else {
    // ブラウザ環境ではスキップ
    expect(true).toBe(true);
  }
});

モジュール解決の問題

異なる環境では、モジュールの解決方法が異なる場合があります。

エラー 3: モジュール解決エラー

javascript// エラーが発生するコード
import { someFunction } from './utils'; // 環境によって解決できない場合がある

// 解決策: 絶対パスを使用
import { someFunction } from '@/utils';
// または
const { someFunction } = require('./utils');

Jest 設定でのモジュール解決設定:

javascript// jest.config.js
module.exports = {
  moduleDirectories: ['node_modules', 'src'],
  moduleNameMapping: {
    '^@/(.*)$': '<rootDir>/src/$1',
    '^~/(.*)$': '<rootDir>/src/$1',
  },
  moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx', 'json'],
};

パフォーマンスの最適化

複数環境でテストを実行する場合、パフォーマンスの最適化が重要になります。

並列実行の設定:

javascript// jest.config.js
module.exports = {
  maxWorkers: '50%', // CPUコアの50%を使用
  maxConcurrency: 5, // 同時実行数を制限

  // キャッシュの設定
  cache: true,
  cacheDirectory: '<rootDir>/.jest-cache',

  // テストタイムアウトの設定
  testTimeout: 10000,
};

テストの並列実行スクリプト:

json{
  "scripts": {
    "test:parallel": "concurrently \"yarn test:node\" \"yarn test:browser\"",
    "test:sequential": "yarn test:node && yarn test:browser"
  }
}

まとめ

Jest で複数環境(Node.js/ブラウザ)のテストを両立することは、現代の Web 開発において非常に重要なスキルです。

この記事で紹介した方法を実践することで、以下のようなメリットを得ることができます:

品質の向上

  • 環境固有のバグを早期に発見
  • フロントエンド・バックエンド両方の品質保証
  • 統合的なテストカバレッジの実現

開発効率の向上

  • 環境別のテスト実行による迅速なフィードバック
  • 自動化されたテストプロセス
  • デバッグ時間の短縮

保守性の向上

  • 明確なテスト構造
  • 環境別の設定分離
  • 再利用可能なテストコード

複数環境でのテスト実行は、最初は複雑に感じるかもしれませんが、適切な設定と戦略を持つことで、非常に強力な開発ツールとなります。

実際のプロジェクトでこの方法を適用する際は、段階的に導入することをお勧めします。まずは基本的な環境設定から始めて、徐々に高度な機能を追加していくことで、チーム全体が無理なく習得できます。

テストは開発の品質を保証する重要な要素です。複数環境でのテスト実行をマスターすることで、より自信を持ってコードを書けるようになり、ユーザーに価値のあるアプリケーションを提供できるようになるでしょう。

関連リンク