T-CREATOR

Jest で E2E テストを導入する:基本と発展テクニック

Jest で E2E テストを導入する:基本と発展テクニック

現代の Web 開発において、品質保証はもはや選択肢ではありません。ユーザーが期待するのは、完璧に動作するアプリケーションです。しかし、手動テストだけでは限界があります。そこで登場するのが、Jest を使った E2E テストです。

E2E テストは、ユーザーの視点からアプリケーション全体をテストする手法です。ログインからログアウトまで、実際のユーザーが行う操作を自動化することで、本番環境での問題を事前に発見できます。

この記事では、Jest を使った E2E テストの導入から発展的なテクニックまで、実践的な内容をお届けします。初心者の方でも安心して始められるよう、段階的に解説していきます。

Jest と E2E テストの基本概念

Jest とは何か

Jest は、Facebook が開発した JavaScript のテストフレームワークです。シンプルさと強力さを兼ね備え、多くの開発者に愛されています。

Jest の特徴は以下の通りです:

  • ゼロコンフィグ: 設定なしで即座にテストを開始できる
  • スナップショットテスト: UI の変更を効率的に検出
  • モック機能: 依存関係を簡単にモック化
  • 並列実行: テストの実行速度を大幅に向上
  • 豊富なマッチャー: 直感的なアサーション
javascript// Jest の基本的なテスト例
describe('計算機能のテスト', () => {
  test('1 + 1 は 2 になる', () => {
    expect(1 + 1).toBe(2);
  });

  test('配列の長さを確認', () => {
    const fruits = ['apple', 'banana', 'orange'];
    expect(fruits).toHaveLength(3);
  });
});

E2E テストの定義と特徴

E2E テスト(End-to-End Testing)は、アプリケーション全体をユーザーの視点でテストする手法です。単体テストや統合テストとは異なり、実際のブラウザ環境でテストを実行します。

E2E テストの特徴:

  • ユーザー視点: 実際のユーザー操作をシミュレート
  • 全体テスト: フロントエンドからバックエンドまで包括的にテスト
  • ブラウザ環境: 実際のブラウザでテストを実行
  • 信頼性: 本番環境に近い状態でテスト
javascript// E2E テストの基本的な構造
describe('ログインフローのテスト', () => {
  test('正常なログインができる', async () => {
    // ブラウザを開く
    await page.goto('http://localhost:3000/login');

    // ユーザー名を入力
    await page.fill('#username', 'testuser');

    // パスワードを入力
    await page.fill('#password', 'password123');

    // ログインボタンをクリック
    await page.click('#login-button');

    // ダッシュボードに遷移することを確認
    await expect(page).toHaveURL(
      'http://localhost:3000/dashboard'
    );
  });
});

他のテスト手法との違い

テストには複数のレベルがあります。それぞれの役割と違いを理解することで、適切なテスト戦略を立てられます。

テスト種別範囲実行環境実行速度信頼性
単体テスト関数・クラスNode.js高速
統合テストモジュール間Node.js中速中高
E2E テストアプリケーション全体ブラウザ低速
javascript// 単体テストの例
test('ユーザー名のバリデーション', () => {
  expect(validateUsername('john')).toBe(true);
  expect(validateUsername('')).toBe(false);
});

// 統合テストの例
test('ユーザー作成API', async () => {
  const response = await request(app)
    .post('/api/users')
    .send({ name: 'John', email: 'john@example.com' });

  expect(response.status).toBe(201);
});

// E2E テストの例
test('ユーザー登録フロー', async () => {
  await page.goto('/register');
  await page.fill('#name', 'John');
  await page.fill('#email', 'john@example.com');
  await page.click('#submit');

  await expect(
    page.locator('.success-message')
  ).toBeVisible();
});

Jest での E2E テスト環境構築

必要なパッケージのインストール

Jest で E2E テストを実行するには、いくつかのパッケージが必要です。Yarn を使って効率的にインストールしましょう。

bash# 基本的な Jest パッケージ
yarn add --dev jest @types/jest

# E2E テスト用の Playwright
yarn add --dev @playwright/test

# 型定義
yarn add --dev @types/node

Playwright は、Microsoft が開発した E2E テストライブラリです。Jest との相性が良く、高速で安定したテストを実行できます。

bash# Playwright のブラウザをインストール
npx playwright install

# 特定のブラウザのみインストール
npx playwright install chromium
npx playwright install firefox
npx playwright install webkit

設定ファイルの作成

Jest の設定ファイルを作成して、E2E テストに最適化された環境を構築します。

javascript// jest.config.js
module.exports = {
  // テスト環境の設定
  testEnvironment: 'node',

  // テストファイルのパターン
  testMatch: [
    '**/__tests__/**/*.test.js',
    '**/e2e/**/*.test.js',
  ],

  // タイムアウト設定(E2E テストは時間がかかるため)
  testTimeout: 30000,

  // 並列実行の設定
  maxWorkers: 1, // E2E テストは並列実行を避ける

  // レポート出力
  verbose: true,

  // カバレッジ設定
  collectCoverage: false, // E2E テストでは不要

  // セットアップファイル
  setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
};
javascript// jest.setup.js
// グローバルなセットアップ
beforeAll(async () => {
  // テストデータベースの初期化
  console.log('テスト環境のセットアップを開始します');
});

afterAll(async () => {
  // クリーンアップ処理
  console.log('テスト環境のクリーンアップを実行します');
});

// 各テストの前後処理
beforeEach(async () => {
  // テストデータの準備
});

afterEach(async () => {
  // テストデータのクリーンアップ
});

基本的なディレクトリ構造

プロジェクトの規模に応じて、適切なディレクトリ構造を作成します。

arduinoproject-root/
├── src/
│   ├── components/
│   ├── pages/
│   └── utils/
├── tests/
│   ├── unit/
│   ├── integration/
│   └── e2e/
│       ├── fixtures/
│       ├── pages/
│       └── utils/
├── jest.config.js
├── jest.setup.js
└── package.json
javascript// tests/e2e/utils/test-utils.js
// テスト用のユーティリティ関数
export const createTestUser = async () => {
  return {
    username: `testuser_${Date.now()}`,
    email: `test_${Date.now()}@example.com`,
    password: 'testpassword123',
  };
};

export const waitForElement = async (
  page,
  selector,
  timeout = 5000
) => {
  await page.waitForSelector(selector, { timeout });
};

export const takeScreenshot = async (page, name) => {
  await page.screenshot({
    path: `./screenshots/${name}_${Date.now()}.png`,
  });
};

基本的な E2E テストの書き方

テストファイルの構造

E2E テストファイルは、明確な構造を持つことで保守性を高められます。

javascript// tests/e2e/login.test.js
const { test, expect } = require('@playwright/test');

// テストグループの定義
describe('ログイン機能の E2E テスト', () => {
  let page;

  // 各テストの前処理
  beforeEach(async ({ browser }) => {
    page = await browser.newPage();
    await page.goto('http://localhost:3000');
  });

  // 各テストの後処理
  afterEach(async () => {
    await page.close();
  });

  // 正常系のテスト
  test('正常なログインができる', async () => {
    // テストの実装
  });

  // 異常系のテスト
  test('無効な認証情報でログインできない', async () => {
    // テストの実装
  });
});

基本的なアサーション

Jest と Playwright を組み合わせた、強力なアサーション機能を活用します。

javascript// 要素の存在確認
test('ログインフォームが表示される', async ({ page }) => {
  await page.goto('/login');

  // 要素が存在することを確認
  await expect(page.locator('#login-form')).toBeVisible();

  // 要素が存在しないことを確認
  await expect(
    page.locator('.error-message')
  ).not.toBeVisible();

  // 要素の数を確認
  const buttons = page.locator('button');
  await expect(buttons).toHaveCount(2);
});
javascript// テキスト内容の確認
test('エラーメッセージが正しく表示される', async ({
  page,
}) => {
  await page.goto('/login');

  // 無効な認証情報でログイン
  await page.fill('#username', 'invalid');
  await page.fill('#password', 'wrong');
  await page.click('#login-button');

  // エラーメッセージの確認
  await expect(
    page.locator('.error-message')
  ).toContainText(
    'ユーザー名またはパスワードが正しくありません'
  );

  // 部分一致での確認
  await expect(page.locator('.error-message')).toHaveText(
    /ユーザー名またはパスワード/
  );
});
javascript// URL とナビゲーションの確認
test('ログイン後にダッシュボードに遷移する', async ({
  page,
}) => {
  await page.goto('/login');

  // 正常なログイン
  await page.fill('#username', 'testuser');
  await page.fill('#password', 'password123');
  await page.click('#login-button');

  // URL の確認
  await expect(page).toHaveURL(
    'http://localhost:3000/dashboard'
  );

  // リダイレクトの確認
  await expect(page).toHaveURL(/.*dashboard/);
});

ページオブジェクトパターンの導入

ページオブジェクトパターンを使うことで、テストコードの保守性と可読性を大幅に向上できます。

javascript// tests/e2e/pages/LoginPage.js
class LoginPage {
  constructor(page) {
    this.page = page;

    // セレクターの定義
    this.selectors = {
      usernameInput: '#username',
      passwordInput: '#password',
      loginButton: '#login-button',
      errorMessage: '.error-message',
      successMessage: '.success-message',
    };
  }

  // ページに移動
  async goto() {
    await this.page.goto('http://localhost:3000/login');
  }

  // ログイン実行
  async login(username, password) {
    await this.page.fill(
      this.selectors.usernameInput,
      username
    );
    await this.page.fill(
      this.selectors.passwordInput,
      password
    );
    await this.page.click(this.selectors.loginButton);
  }

  // エラーメッセージの取得
  async getErrorMessage() {
    return await this.page.textContent(
      this.selectors.errorMessage
    );
  }

  // 成功メッセージの確認
  async isSuccessMessageVisible() {
    return await this.page.isVisible(
      this.selectors.successMessage
    );
  }
}

module.exports = LoginPage;
javascript// tests/e2e/login-with-pom.test.js
const LoginPage = require('./pages/LoginPage');

describe('ページオブジェクトを使ったログインテスト', () => {
  let loginPage;

  beforeEach(async ({ page }) => {
    loginPage = new LoginPage(page);
    await loginPage.goto();
  });

  test('正常なログインができる', async ({ page }) => {
    await loginPage.login('testuser', 'password123');

    // ダッシュボードに遷移することを確認
    await expect(page).toHaveURL(/.*dashboard/);
  });

  test('無効な認証情報でエラーが表示される', async ({
    page,
  }) => {
    await loginPage.login('invalid', 'wrong');

    const errorMessage = await loginPage.getErrorMessage();
    expect(errorMessage).toContain(
      'ユーザー名またはパスワードが正しくありません'
    );
  });
});

発展的な E2E テストテクニック

カスタムマッチャーの作成

Jest のカスタムマッチャーを作成することで、より直感的で読みやすいテストを書けます。

javascript// tests/e2e/matchers/custom-matchers.js
expect.extend({
  // 要素が表示されるまで待機するマッチャー
  async toBeVisibleWithTimeout(received, timeout = 5000) {
    const pass = await received.isVisible({ timeout });

    if (pass) {
      return {
        message: () =>
          `要素が ${timeout}ms 以内に表示されました`,
        pass: true,
      };
    } else {
      return {
        message: () =>
          `要素が ${timeout}ms 以内に表示されませんでした`,
        pass: false,
      };
    }
  },

  // 要素のテキストが期待値と一致するマッチャー
  async toHaveTextContent(received, expectedText) {
    const actualText = await received.textContent();
    const pass = actualText.includes(expectedText);

    if (pass) {
      return {
        message: () =>
          `要素のテキスト "${actualText}" に "${expectedText}" が含まれています`,
        pass: true,
      };
    } else {
      return {
        message: () =>
          `要素のテキスト "${actualText}" に "${expectedText}" が含まれていません`,
        pass: false,
      };
    }
  },
});
javascript// jest.setup.js に追加
require('./tests/e2e/matchers/custom-matchers');

// カスタムマッチャーの使用例
test('カスタムマッチャーを使ったテスト', async ({
  page,
}) => {
  await page.goto('/dashboard');

  const welcomeMessage = page.locator('.welcome-message');

  // カスタムマッチャーを使用
  await expect(welcomeMessage).toBeVisibleWithTimeout(3000);
  await expect(welcomeMessage).toHaveTextContent(
    'ようこそ'
  );
});

モックとスタブの活用

E2E テストでも、外部 API やデータベースをモックすることで、テストの安定性を向上できます。

javascript// tests/e2e/mocks/api-mocks.js
// API レスポンスのモック
const mockApiResponses = {
  login: {
    success: {
      status: 200,
      body: {
        token: 'mock-jwt-token',
        user: {
          id: 1,
          username: 'testuser',
          email: 'test@example.com',
        },
      },
    },
    failure: {
      status: 401,
      body: {
        error: 'Invalid credentials',
      },
    },
  },
};

// API のモック設定
const setupApiMocks = (page) => {
  // ログインAPI のモック
  page.route('**/api/login', async (route) => {
    const postData = route.request().postDataJSON();

    if (
      postData.username === 'testuser' &&
      postData.password === 'password123'
    ) {
      await route.fulfill({
        status: 200,
        contentType: 'application/json',
        body: JSON.stringify(
          mockApiResponses.login.success.body
        ),
      });
    } else {
      await route.fulfill({
        status: 401,
        contentType: 'application/json',
        body: JSON.stringify(
          mockApiResponses.login.failure.body
        ),
      });
    }
  });
};

module.exports = { setupApiMocks };
javascript// モックを使ったテスト例
const { setupApiMocks } = require('./mocks/api-mocks');

test('モックAPI を使ったログインテスト', async ({
  page,
}) => {
  // API モックを設定
  setupApiMocks(page);

  await page.goto('/login');

  // 正常なログイン
  await page.fill('#username', 'testuser');
  await page.fill('#password', 'password123');
  await page.click('#login-button');

  // モックレスポンスに基づいてテスト
  await expect(page).toHaveURL('/dashboard');
  await expect(page.locator('.user-info')).toContainText(
    'testuser'
  );
});

並列実行とパフォーマンス最適化

大規模なテストスイートでは、並列実行とパフォーマンス最適化が重要です。

javascript// jest.config.js の並列実行設定
module.exports = {
  // ワーカー数の設定
  maxWorkers: process.env.CI ? 2 : '50%',

  // テストの並列実行設定
  testRunner: 'jest-circus/runner',

  // タイムアウト設定
  testTimeout: 30000,

  // テストの実行順序
  testSequencer: './test-sequencer.js',
};
javascript// test-sequencer.js
// テストの実行順序を制御
const TestSequencer =
  require('@jest/test-sequencer').default;

class CustomSequencer extends TestSequencer {
  sort(tests) {
    // 高速なテストを先に実行
    return tests.sort((testA, testB) => {
      const isFastA = testA.path.includes('fast');
      const isFastB = testB.path.includes('fast');

      if (isFastA && !isFastB) return -1;
      if (!isFastA && isFastB) return 1;
      return 0;
    });
  }
}

module.exports = CustomSequencer;
javascript// パフォーマンス最適化の例
describe('最適化されたテストスイート', () => {
  // 共有のセットアップ
  beforeAll(async ({ browser }) => {
    // ブラウザコンテキストを共有
    context = await browser.newContext({
      viewport: { width: 1280, height: 720 },
    });
  });

  // テスト間でページを再利用
  beforeEach(async () => {
    page = await context.newPage();
  });

  afterEach(async () => {
    await page.close();
  });

  afterAll(async () => {
    await context.close();
  });

  test('高速なテスト', async () => {
    // 軽量なテスト
  });
});

実際のプロジェクトでの活用例

ログインフロー

実際のプロジェクトでよく使われるログインフローのテスト例です。

javascript// tests/e2e/flows/login-flow.test.js
const LoginPage = require('../pages/LoginPage');
const DashboardPage = require('../pages/DashboardPage');

describe('ログインフローの統合テスト', () => {
  let loginPage, dashboardPage;

  beforeEach(async ({ page }) => {
    loginPage = new LoginPage(page);
    dashboardPage = new DashboardPage(page);
  });

  test('新規ユーザーのログインフロー', async ({ page }) => {
    // 1. ログインページにアクセス
    await loginPage.goto();

    // 2. 新規登録リンクをクリック
    await page.click('#register-link');
    await expect(page).toHaveURL('/register');

    // 3. ユーザー情報を入力
    await page.fill('#username', 'newuser');
    await page.fill('#email', 'newuser@example.com');
    await page.fill('#password', 'newpassword123');
    await page.fill('#confirm-password', 'newpassword123');

    // 4. 登録ボタンをクリック
    await page.click('#register-button');

    // 5. 確認メッセージを確認
    await expect(
      page.locator('.success-message')
    ).toContainText('アカウントが正常に作成されました');

    // 6. 自動ログインされることを確認
    await expect(page).toHaveURL('/dashboard');
    await expect(
      dashboardPage.getWelcomeMessage()
    ).toContainText('newuser');
  });

  test('既存ユーザーのログインフロー', async ({ page }) => {
    // 1. ログインページにアクセス
    await loginPage.goto();

    // 2. 認証情報を入力
    await loginPage.login('existinguser', 'password123');

    // 3. ダッシュボードに遷移することを確認
    await expect(page).toHaveURL('/dashboard');

    // 4. ユーザー情報が正しく表示されることを確認
    await expect(dashboardPage.getUserInfo()).toContainText(
      'existinguser'
    );

    // 5. ログアウト機能をテスト
    await dashboardPage.logout();
    await expect(page).toHaveURL('/login');
  });

  test('エラーハンドリングのテスト', async ({ page }) => {
    await loginPage.goto();

    // 無効な認証情報でログイン
    await loginPage.login('invalid', 'wrong');

    // エラーメッセージの確認
    const errorMessage = await loginPage.getErrorMessage();
    expect(errorMessage).toContain(
      'ユーザー名またはパスワードが正しくありません'
    );

    // ログインフォームがクリアされないことを確認
    await expect(page.locator('#username')).toHaveValue(
      'invalid'
    );
    await expect(page.locator('#password')).toHaveValue('');
  });
});

商品検索機能

EC サイトなどで重要な商品検索機能のテスト例です。

javascript// tests/e2e/flows/product-search.test.js
const SearchPage = require('../pages/SearchPage');
const ProductPage = require('../pages/ProductPage');

describe('商品検索機能のテスト', () => {
  let searchPage, productPage;

  beforeEach(async ({ page }) => {
    searchPage = new SearchPage(page);
    productPage = new ProductPage(page);
    await page.goto('/search');
  });

  test('キーワード検索の基本機能', async ({ page }) => {
    // 1. 検索キーワードを入力
    await searchPage.search('ノートパソコン');

    // 2. 検索結果が表示されることを確認
    await expect(
      page.locator('.search-results')
    ).toBeVisible();

    // 3. 検索結果の件数を確認
    const resultCount = await page
      .locator('.product-item')
      .count();
    expect(resultCount).toBeGreaterThan(0);

    // 4. 検索結果にキーワードが含まれていることを確認
    const firstProduct = page
      .locator('.product-item')
      .first();
    await expect(
      firstProduct.locator('.product-name')
    ).toContainText('ノートパソコン');
  });

  test('フィルター機能のテスト', async ({ page }) => {
    // 1. 価格フィルターを設定
    await searchPage.setPriceFilter(50000, 100000);

    // 2. ブランドフィルターを設定
    await searchPage.selectBrand('Apple');

    // 3. フィルター適用ボタンをクリック
    await page.click('#apply-filters');

    // 4. フィルター条件が適用されることを確認
    await expect(
      page.locator('.active-filters')
    ).toContainText('¥50,000 - ¥100,000');
    await expect(
      page.locator('.active-filters')
    ).toContainText('Apple');

    // 5. 検索結果がフィルター条件に合致することを確認
    const products = page.locator('.product-item');
    const productCount = await products.count();

    for (let i = 0; i < Math.min(productCount, 3); i++) {
      const product = products.nth(i);
      const price = await product
        .locator('.price')
        .textContent();
      const priceValue = parseInt(
        price.replace(/[^\d]/g, '')
      );

      expect(priceValue).toBeGreaterThanOrEqual(50000);
      expect(priceValue).toBeLessThanOrEqual(100000);
    }
  });

  test('ソート機能のテスト', async ({ page }) => {
    // 1. 価格の安い順でソート
    await searchPage.sortBy('price-asc');

    // 2. 最初の商品の価格を取得
    const firstPrice = await page
      .locator('.product-item')
      .first()
      .locator('.price')
      .textContent();
    const firstPriceValue = parseInt(
      firstPrice.replace(/[^\d]/g, '')
    );

    // 3. 価格の高い順でソート
    await searchPage.sortBy('price-desc');

    // 4. 最後の商品の価格を取得
    const lastPrice = await page
      .locator('.product-item')
      .last()
      .locator('.price')
      .textContent();
    const lastPriceValue = parseInt(
      lastPrice.replace(/[^\d]/g, '')
    );

    // 5. ソートが正しく機能することを確認
    expect(firstPriceValue).toBeLessThan(lastPriceValue);
  });
});

決済プロセス

EC サイトの決済プロセスのテスト例です。

javascript// tests/e2e/flows/checkout-flow.test.js
const CartPage = require('../pages/CartPage');
const CheckoutPage = require('../pages/CheckoutPage');
const PaymentPage = require('../pages/PaymentPage');

describe('決済プロセスのテスト', () => {
  let cartPage, checkoutPage, paymentPage;

  beforeEach(async ({ page }) => {
    cartPage = new CartPage(page);
    checkoutPage = new CheckoutPage(page);
    paymentPage = new PaymentPage(page);
  });

  test('正常な決済フロー', async ({ page }) => {
    // 1. カートに商品を追加
    await page.goto('/products/laptop');
    await page.click('#add-to-cart');

    // 2. カートページに移動
    await page.goto('/cart');
    await expect(page.locator('.cart-item')).toHaveCount(1);

    // 3. チェックアウトに進む
    await page.click('#proceed-to-checkout');
    await expect(page).toHaveURL('/checkout');

    // 4. 配送情報を入力
    await checkoutPage.fillShippingInfo({
      firstName: '田中',
      lastName: '太郎',
      email: 'tanaka@example.com',
      phone: '090-1234-5678',
      address: '東京都渋谷区1-1-1',
      postalCode: '150-0001',
    });

    // 5. 支払い方法を選択
    await checkoutPage.selectPaymentMethod('credit-card');

    // 6. 支払い情報を入力
    await paymentPage.fillPaymentInfo({
      cardNumber: '4111111111111111',
      expiryMonth: '12',
      expiryYear: '2025',
      cvv: '123',
      cardholderName: 'TANAKA TARO',
    });

    // 7. 注文を確定
    await page.click('#place-order');

    // 8. 注文完了ページに遷移することを確認
    await expect(page).toHaveURL(/.*order-confirmation/);
    await expect(
      page.locator('.order-success')
    ).toBeVisible();

    // 9. 注文番号が表示されることを確認
    const orderNumber = await page
      .locator('.order-number')
      .textContent();
    expect(orderNumber).toMatch(/^[A-Z0-9]{8}$/);
  });

  test('決済エラーのハンドリング', async ({ page }) => {
    await page.goto('/checkout');

    // 無効なカード情報で決済
    await paymentPage.fillPaymentInfo({
      cardNumber: '4000000000000002', // 決済拒否カード
      expiryMonth: '12',
      expiryYear: '2025',
      cvv: '123',
      cardholderName: 'TEST USER',
    });

    await page.click('#place-order');

    // エラーメッセージが表示されることを確認
    await expect(
      page.locator('.payment-error')
    ).toBeVisible();
    await expect(
      page.locator('.payment-error')
    ).toContainText('決済が拒否されました');

    // チェックアウトページに留まることを確認
    await expect(page).toHaveURL('/checkout');
  });

  test('在庫切れ商品の処理', async ({ page }) => {
    // 在庫切れ商品をカートに追加
    await page.goto('/products/out-of-stock-item');
    await page.click('#add-to-cart');

    await page.goto('/cart');

    // 在庫切れ警告が表示されることを確認
    await expect(
      page.locator('.out-of-stock-warning')
    ).toBeVisible();

    // チェックアウトボタンが無効化されることを確認
    await expect(
      page.locator('#proceed-to-checkout')
    ).toBeDisabled();
  });
});

デバッグとトラブルシューティング

よくある問題と解決策

E2E テストでよく遭遇する問題とその解決策を紹介します。

javascript// 問題1: 要素が見つからないエラー
// エラー: Error: Timeout 5000ms exceeded while waiting for selector "#login-button"

// 解決策: より堅牢なセレクターと待機処理
test('堅牢な要素待機の例', async ({ page }) => {
  await page.goto('/login');

  // 良い例: 複数のセレクターを試す
  const loginButton = page.locator(
    '#login-button, button[type="submit"], .login-btn'
  );
  await expect(loginButton).toBeVisible({ timeout: 10000 });

  // 良い例: 要素の状態を確認してから操作
  await loginButton.waitFor({ state: 'visible' });
  await loginButton.click();
});
javascript// 問題2: フレイキーテスト(時々失敗するテスト)
// 原因: 非同期処理の待機不足

// 解決策: 適切な待機処理
test('安定した非同期処理のテスト', async ({ page }) => {
  await page.goto('/dashboard');

  // 良い例: ネットワークアイドルを待つ
  await page.waitForLoadState('networkidle');

  // 良い例: 特定の要素が読み込まれるまで待つ
  await page.waitForSelector('.user-data', {
    state: 'attached',
  });

  // 良い例: 条件が満たされるまで待つ
  await page.waitForFunction(() => {
    return (
      document.querySelectorAll('.product-item').length > 0
    );
  });

  // テストの実行
  const productCount = await page
    .locator('.product-item')
    .count();
  expect(productCount).toBeGreaterThan(0);
});
javascript// 問題3: タイムアウトエラー
// エラー: Error: Test timeout of 30000ms exceeded

// 解決策: タイムアウト設定の最適化
describe('タイムアウト対策のテスト', () => {
  // 個別のテストでタイムアウトを設定
  test('長時間の処理を含むテスト', async ({ page }) => {
    // テスト固有のタイムアウト設定
    test.setTimeout(60000);

    await page.goto('/slow-loading-page');

    // 明示的な待機処理
    await page.waitForSelector('.content-loaded', {
      timeout: 30000,
    });

    // テストの実行
    await expect(page.locator('.content')).toBeVisible();
  }, 60000); // タイムアウトを60秒に設定
});
javascript// 問題4: ブラウザの互換性問題
// 解決策: 複数ブラウザでのテスト実行

// playwright.config.js
module.exports = {
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
  ],
};

// ブラウザ固有の処理
test('ブラウザ固有の処理', async ({
  page,
  browserName,
}) => {
  await page.goto('/test-page');

  // ブラウザ固有の処理
  if (browserName === 'firefox') {
    // Firefox 固有の処理
    await page.waitForTimeout(1000);
  } else if (browserName === 'webkit') {
    // Safari 固有の処理
    await page.waitForTimeout(500);
  }

  // 共通のテスト
  await expect(page.locator('.test-element')).toBeVisible();
});

ログとレポートの活用

効果的なデバッグのために、ログとレポートを活用します。

javascript// 詳細なログ出力の設定
// jest.config.js
module.exports = {
  verbose: true,
  reporters: [
    'default',
    [
      'jest-html-reporters',
      {
        publicPath: './reports',
        filename: 'e2e-test-report.html',
        expand: true,
      },
    ],
  ],
};
javascript// テスト内でのログ出力
test('詳細なログ付きテスト', async ({ page }) => {
  console.log('テスト開始: ログインフローのテスト');

  await page.goto('/login');
  console.log('ログインページにアクセス完了');

  // スクリーンショットを撮影
  await page.screenshot({
    path: './screenshots/login-page.png',
  });

  await page.fill('#username', 'testuser');
  console.log('ユーザー名入力完了');

  await page.fill('#password', 'password123');
  console.log('パスワード入力完了');

  await page.click('#login-button');
  console.log('ログインボタンクリック完了');

  // ネットワークリクエストの監視
  page.on('request', (request) => {
    console.log(
      `リクエスト: ${request.method()} ${request.url()}`
    );
  });

  page.on('response', (response) => {
    console.log(
      `レスポンス: ${response.status()} ${response.url()}`
    );
  });

  await expect(page).toHaveURL('/dashboard');
  console.log('ダッシュボードへの遷移確認完了');

  // 最終スクリーンショット
  await page.screenshot({
    path: './screenshots/dashboard.png',
  });
  console.log('テスト完了: ログインフローのテスト');
});
javascript// カスタムレポートの作成
// tests/e2e/utils/test-reporter.js
class CustomTestReporter {
  constructor() {
    this.results = [];
  }

  onTestStart(test) {
    console.log(`🧪 テスト開始: ${test.name}`);
    this.currentTest = {
      name: test.name,
      startTime: Date.now(),
      status: 'running',
    };
  }

  onTestEnd(test, result) {
    const duration =
      Date.now() - this.currentTest.startTime;
    this.currentTest.duration = duration;
    this.currentTest.status = result.status;

    if (result.status === 'passed') {
      console.log(
        `✅ テスト成功: ${test.name} (${duration}ms)`
      );
    } else {
      console.log(
        `❌ テスト失敗: ${test.name} (${duration}ms)`
      );
      console.log(`   エラー: ${result.error.message}`);
    }

    this.results.push(this.currentTest);
  }

  onRunComplete() {
    const summary = {
      total: this.results.length,
      passed: this.results.filter(
        (r) => r.status === 'passed'
      ).length,
      failed: this.results.filter(
        (r) => r.status === 'failed'
      ).length,
      totalDuration: this.results.reduce(
        (sum, r) => sum + r.duration,
        0
      ),
    };

    console.log('\n📊 テスト結果サマリー:');
    console.log(`   総テスト数: ${summary.total}`);
    console.log(`   成功: ${summary.passed}`);
    console.log(`   失敗: ${summary.failed}`);
    console.log(
      `   総実行時間: ${summary.totalDuration}ms`
    );
  }
}

module.exports = CustomTestReporter;

まとめ

Jest を使った E2E テストの導入は、アプリケーションの品質向上に大きく貢献します。この記事で紹介した内容を実践することで、以下のメリットを得られます:

品質の向上

  • ユーザー視点での包括的なテスト
  • 本番環境での問題を事前に発見
  • 回帰テストの自動化

開発効率の向上

  • 手動テストの時間短縮
  • デバッグ時間の削減
  • 自信を持ったデプロイ

チーム開発の改善

  • テスト結果の共有
  • 品質基準の統一
  • 継続的インテグレーション

E2E テストは最初は複雑に感じるかもしれませんが、段階的に導入することで、確実にスキルを向上させられます。まずは小さなテストから始めて、徐々に範囲を広げていくことをお勧めします。

テストコードは、アプリケーションコードと同じくらい重要です。読みやすく、保守しやすいテストを書くことで、長期的なプロジェクトの成功につながります。

最後に、E2E テストは完璧ではありませんが、適切に活用することで、ユーザーに信頼されるアプリケーションを構築できます。継続的な改善と学習を心がけ、より良いテストを書いていきましょう。

関連リンク