0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

E2Eテストの基礎から学ぶ:実践的なEnd-to-Endテストガイド

0
Posted at

はじめに

「テストは書いてるけど、本番環境で動かしたら想定外のエラーが...」という経験、ありませんか?

ユニットテストで個々の関数は完璧に動いていても、実際にユーザーが操作する画面では思わぬ問題が起きることがあります。ボタンをクリックしたのにフォームが送信されない、画面遷移がうまくいかない、APIとの連携でエラーが発生する...こういった問題を事前に見つけるのがE2Eテストです。

この記事では、E2E(End-to-End)テストの基礎から実践的な使い方まで、できるだけわかりやすく解説していきます。初めての方でも安心して読み進められるよう、丁寧に説明していきますね。

E2Eテストとは何か

E2E(End-to-End)テストは、アプリケーション全体を通しての動作を検証するテスト手法です。

テストの階層構造

ソフトウェアテストには、いくつかの階層があります:

┌─────────────────────────┐
│    E2Eテスト            │ ← ユーザー視点で全体を検証
├─────────────────────────┤
│  統合テスト             │ ← 複数のモジュールの連携を検証
├─────────────────────────┤
│  ユニットテスト         │ ← 個々の関数やクラスを検証
└─────────────────────────┘

それぞれのテストには役割があります:

ユニットテスト

  • 個々の関数やメソッドが正しく動くかを検証
  • 実行が速く、問題の特定がしやすい
  • 例:「この計算関数は正しい値を返すか?」

統合テスト

  • 複数のモジュールやコンポーネントが正しく連携するかを検証
  • 例:「データベースとAPIが正しく連携するか?」

E2Eテスト

  • ユーザーの操作フロー全体が正しく動くかを検証
  • 実際のブラウザで動作を確認
  • 例:「ログインしてから商品を購入し、注文完了画面が表示されるまでの流れが正しいか?」

E2Eテストの特徴

E2Eテストは、まさに「ユーザーになりきって」アプリケーションをテストします。

// E2Eテストの例(イメージ)
test('ユーザーが商品を購入できる', async () => {
  // ページを開く
  await page.goto('https://example.com');
  
  // ログインボタンをクリック
  await page.click('#login-button');
  
  // メールアドレスとパスワードを入力
  await page.fill('#email', 'user@example.com');
  await page.fill('#password', 'password123');
  await page.click('#submit-login');
  
  // 商品ページに移動
  await page.click('#product-1');
  
  // カートに追加
  await page.click('#add-to-cart');
  
  // 購入手続き
  await page.click('#checkout');
  
  // 注文完了を確認
  await expect(page.locator('#order-confirmation')).toBeVisible();
});

このテストは、実際のユーザーがブラウザで操作する流れをそのまま再現しています。

なぜE2Eテストが必要なのか

「ユニットテストと統合テストがあれば十分じゃない?」と思うかもしれません。でも、以下のような問題はE2Eテストでしか見つけられません:

実際のブラウザでしか起きない問題

  • CSSのレイアウト崩れでボタンがクリックできない
  • 特定のブラウザでのみ発生するJavaScriptエラー
  • 画面の読み込み順序による問題

複雑な状態遷移の問題

  • ログイン状態の管理ミス
  • 複数画面をまたぐデータの受け渡しエラー
  • セッションやCookieの問題

外部サービスとの連携問題

  • 決済APIとの連携エラー
  • 認証サービスとの連携不具合
  • 外部APIのレスポンス形式変更による影響

これらは個々のコンポーネントを単体でテストしても見つかりません。全体を通して初めて見えてくる問題なのです。

E2Eテストの基本的な流れ

E2Eテストは、一般的に以下のような流れで実行されます。

1. 準備(Setup)

テストを実行する前の準備段階です:

  • ブラウザの起動
  • テスト用のデータベースの準備
  • 必要に応じてテストユーザーの作成
  • アプリケーションへのアクセス
// テストの準備例
beforeEach(async () => {
  // ブラウザを起動
  browser = await chromium.launch();
  page = await browser.newPage();
  
  // テストデータをセットアップ
  await setupTestDatabase();
  
  // アプリケーションのトップページを開く
  await page.goto('http://localhost:3000');
});

2. 実行(Execute)

実際のユーザー操作をシミュレートします:

要素の操作

  • クリック、入力、選択など
  • スクロール、ホバーなど
// ボタンをクリック
await page.click('button#submit');

// テキストを入力
await page.fill('input[name="username"]', '太郎');

// セレクトボックスから選択
await page.selectOption('select#prefecture', '東京都');

// チェックボックスをチェック
await page.check('input[type="checkbox"]#agree');

画面遷移

  • ページ間の移動
  • URLの変更を待つ
// リンクをクリックして画面遷移を待つ
await page.click('a#next-page');
await page.waitForURL('**/next-page');

非同期処理の待機

  • APIレスポンスを待つ
  • アニメーションの完了を待つ
// 特定の要素が表示されるまで待つ
await page.waitForSelector('#loading', { state: 'hidden' });
await page.waitForSelector('#content', { state: 'visible' });

3. 検証(Assert)

期待通りの結果になっているかを確認します:

// テキストの内容を検証
await expect(page.locator('#message')).toHaveText('登録が完了しました');

// 要素の表示/非表示を検証
await expect(page.locator('#success-banner')).toBeVisible();
await expect(page.locator('#error-message')).toBeHidden();

// URLを検証
await expect(page).toHaveURL('http://localhost:3000/success');

// 要素の数を検証
await expect(page.locator('.product-item')).toHaveCount(10);

4. 後処理(Cleanup)

テスト後のクリーンアップを行います:

afterEach(async () => {
  // スクリーンショットを保存(失敗時のデバッグ用)
  if (testFailed) {
    await page.screenshot({ path: 'test-failure.png' });
  }
  
  // ブラウザを閉じる
  await browser.close();
  
  // テストデータをクリーンアップ
  await cleanupTestDatabase();
});

主要なE2Eテストツール

E2Eテストには、様々なツールがあります。それぞれに特徴があるので、プロジェクトに合ったものを選びましょう。

Playwright

特徴

  • Microsoftが開発した新しいフレームワーク
  • 複数のブラウザ(Chromium、Firefox、WebKit)に対応
  • 高速で安定している
  • 強力なデバッグ機能

こんな時におすすめ

  • モダンなウェブアプリケーション
  • クロスブラウザテストが必要
  • CI/CDでの自動テストを重視
// Playwrightの例
import { test, expect } from '@playwright/test';

test('ログインできる', async ({ page }) => {
  await page.goto('http://localhost:3000/login');
  
  await page.fill('#email', 'test@example.com');
  await page.fill('#password', 'password');
  await page.click('#login-button');
  
  await expect(page).toHaveURL('http://localhost:3000/dashboard');
  await expect(page.locator('#welcome-message')).toContainText('ようこそ');
});

Cypress

特徴

  • 開発者体験に優れたツール
  • リアルタイムでテスト実行を確認できる
  • デバッグがしやすい
  • 豊富なドキュメントとコミュニティ

こんな時におすすめ

  • フロントエンド開発者がメイン
  • テスト開発のスピードを重視
  • ビジュアルなデバッグが必要
// Cypressの例
describe('ログイン機能', () => {
  it('正しい認証情報でログインできる', () => {
    cy.visit('http://localhost:3000/login');
    
    cy.get('#email').type('test@example.com');
    cy.get('#password').type('password');
    cy.get('#login-button').click();
    
    cy.url().should('include', '/dashboard');
    cy.get('#welcome-message').should('contain', 'ようこそ');
  });
});

Selenium

特徴

  • 最も歴史があり、実績豊富
  • 多言語対応(Java、Python、C#など)
  • 幅広いブラウザとプラットフォームに対応

こんな時におすすめ

  • 既存のSeleniumインフラがある
  • JavaScript以外の言語を使いたい
  • レガシーブラウザのサポートが必要
# Seleniumの例(Python)
from selenium import webdriver
from selenium.webdriver.common.by import By

driver = webdriver.Chrome()
driver.get("http://localhost:3000/login")

driver.find_element(By.ID, "email").send_keys("test@example.com")
driver.find_element(By.ID, "password").send_keys("password")
driver.find_element(By.ID, "login-button").click()

assert "/dashboard" in driver.current_url
assert "ようこそ" in driver.find_element(By.ID, "welcome-message").text

driver.quit()

各ツールの比較

特徴 Playwright Cypress Selenium
学習コスト
実行速度 速い 速い やや遅い
デバッグ 優秀 非常に優秀 普通
ブラウザサポート 広い やや限定的 非常に広い
コミュニティ 成長中 活発 非常に大きい
日本語情報 増加中 豊富 非常に豊富

E2Eテストの実践例

実際のシナリオを使って、E2Eテストの書き方を見ていきましょう。ここではPlaywrightを使った例を紹介します。

シナリオ:ECサイトでの商品購入フロー

以下のようなユーザーストーリーをテストします:

ユーザーとして、商品を検索し、カートに追加して、購入手続きを完了したい

ステップ1:環境のセットアップ

# Playwrightのインストール
npm init playwright@latest

# インストール後に自動的にブラウザがダウンロードされます

ステップ2:テストファイルの作成

// tests/purchase-flow.spec.js
import { test, expect } from '@playwright/test';

test.describe('商品購入フロー', () => {
  // 各テストの前に実行される準備
  test.beforeEach(async ({ page }) => {
    // トップページにアクセス
    await page.goto('http://localhost:3000');
  });

  test('ゲストユーザーが商品を購入できる', async ({ page }) => {
    // 1. 商品を検索
    await test.step('商品を検索する', async () => {
      await page.fill('#search-input', 'ノートパソコン');
      await page.click('#search-button');
      
      // 検索結果が表示されるまで待つ
      await page.waitForSelector('.product-list');
      
      // 検索結果が1件以上あることを確認
      const products = await page.locator('.product-item');
      await expect(products).not.toHaveCount(0);
    });

    // 2. 商品詳細ページに移動
    await test.step('商品詳細を表示する', async () => {
      // 最初の商品をクリック
      await page.click('.product-item:first-child');
      
      // 商品詳細が表示されることを確認
      await expect(page.locator('#product-title')).toBeVisible();
      await expect(page.locator('#product-price')).toBeVisible();
    });

    // 3. カートに追加
    await test.step('カートに商品を追加する', async () => {
      // 数量を選択
      await page.selectOption('#quantity', '2');
      
      // カートに追加ボタンをクリック
      await page.click('#add-to-cart');
      
      // 成功メッセージが表示されることを確認
      await expect(page.locator('#cart-notification')).toContainText('カートに追加しました');
      
      // カートアイコンの数が更新されることを確認
      await expect(page.locator('#cart-count')).toHaveText('2');
    });

    // 4. カートを確認
    await test.step('カートの内容を確認する', async () => {
      // カートアイコンをクリック
      await page.click('#cart-icon');
      
      // カートページに遷移
      await expect(page).toHaveURL(/.*\/cart/);
      
      // 商品がカートに入っていることを確認
      await expect(page.locator('.cart-item')).toHaveCount(1);
      await expect(page.locator('#item-quantity')).toHaveText('2');
    });

    // 5. 購入手続き
    await test.step('購入手続きを進める', async () => {
      // レジに進むボタンをクリック
      await page.click('#checkout-button');
      
      // お客様情報入力ページに遷移
      await expect(page).toHaveURL(/.*\/checkout/);
      
      // 配送先情報を入力
      await page.fill('#name', '山田太郎');
      await page.fill('#email', 'yamada@example.com');
      await page.fill('#phone', '090-1234-5678');
      await page.fill('#postal-code', '100-0001');
      await page.selectOption('#prefecture', '東京都');
      await page.fill('#city', '千代田区');
      await page.fill('#address', '千代田1-1-1');
      
      // 支払い方法を選択
      await page.check('#payment-credit-card');
      
      // クレジットカード情報を入力
      await page.fill('#card-number', '4111111111111111');
      await page.fill('#card-name', 'TARO YAMADA');
      await page.selectOption('#card-month', '12');
      await page.selectOption('#card-year', '2025');
      await page.fill('#card-cvv', '123');
      
      // 利用規約に同意
      await page.check('#terms-agreement');
      
      // 注文を確定ボタンをクリック
      await page.click('#submit-order');
    });

    // 6. 注文完了を確認
    await test.step('注文完了画面が表示される', async () => {
      // 注文完了ページに遷移
      await expect(page).toHaveURL(/.*\/order-complete/);
      
      // 成功メッセージの表示を確認
      await expect(page.locator('#order-success-message')).toContainText('ご注文ありがとうございます');
      
      // 注文番号が表示されることを確認
      const orderNumber = await page.locator('#order-number').textContent();
      expect(orderNumber).toMatch(/^[A-Z0-9]+$/);
      
      // スクリーンショットを保存(記録用)
      await page.screenshot({ path: `test-results/order-${orderNumber}.png` });
    });
  });
});

ステップ3:ページオブジェクトパターンで整理

テストコードが長くなると、メンテナンスが大変になります。ページオブジェクトパターンを使うと、コードを整理できます:

// pages/ProductPage.js
export class ProductPage {
  constructor(page) {
    this.page = page;
    this.searchInput = page.locator('#search-input');
    this.searchButton = page.locator('#search-button');
    this.productList = page.locator('.product-list');
    this.productItems = page.locator('.product-item');
    this.addToCartButton = page.locator('#add-to-cart');
    this.cartNotification = page.locator('#cart-notification');
  }

  async searchProduct(keyword) {
    await this.searchInput.fill(keyword);
    await this.searchButton.click();
    await this.productList.waitFor();
  }

  async selectFirstProduct() {
    await this.productItems.first().click();
  }

  async addToCart(quantity = 1) {
    if (quantity > 1) {
      await this.page.selectOption('#quantity', quantity.toString());
    }
    await this.addToCartButton.click();
    await this.cartNotification.waitFor();
  }
}
// pages/CheckoutPage.js
export class CheckoutPage {
  constructor(page) {
    this.page = page;
  }

  async fillShippingInfo(info) {
    await this.page.fill('#name', info.name);
    await this.page.fill('#email', info.email);
    await this.page.fill('#phone', info.phone);
    await this.page.fill('#postal-code', info.postalCode);
    await this.page.selectOption('#prefecture', info.prefecture);
    await this.page.fill('#city', info.city);
    await this.page.fill('#address', info.address);
  }

  async fillPaymentInfo(payment) {
    await this.page.check('#payment-credit-card');
    await this.page.fill('#card-number', payment.cardNumber);
    await this.page.fill('#card-name', payment.cardName);
    await this.page.selectOption('#card-month', payment.month);
    await this.page.selectOption('#card-year', payment.year);
    await this.page.fill('#card-cvv', payment.cvv);
  }

  async submitOrder() {
    await this.page.check('#terms-agreement');
    await this.page.click('#submit-order');
  }
}
// tests/purchase-flow-refactored.spec.js
import { test, expect } from '@playwright/test';
import { ProductPage } from '../pages/ProductPage';
import { CheckoutPage } from '../pages/CheckoutPage';

test('商品購入フロー(リファクタ版)', async ({ page }) => {
  const productPage = new ProductPage(page);
  const checkoutPage = new CheckoutPage(page);

  await page.goto('http://localhost:3000');

  // 商品検索と追加
  await productPage.searchProduct('ノートパソコン');
  await productPage.selectFirstProduct();
  await productPage.addToCart(2);

  // カート確認
  await page.click('#cart-icon');
  await expect(page.locator('.cart-item')).toHaveCount(1);

  // 購入手続き
  await page.click('#checkout-button');
  
  await checkoutPage.fillShippingInfo({
    name: '山田太郎',
    email: 'yamada@example.com',
    phone: '090-1234-5678',
    postalCode: '100-0001',
    prefecture: '東京都',
    city: '千代田区',
    address: '千代田1-1-1'
  });

  await checkoutPage.fillPaymentInfo({
    cardNumber: '4111111111111111',
    cardName: 'TARO YAMADA',
    month: '12',
    year: '2025',
    cvv: '123'
  });

  await checkoutPage.submitOrder();

  // 注文完了確認
  await expect(page).toHaveURL(/.*\/order-complete/);
  await expect(page.locator('#order-success-message')).toBeVisible();
});

ずいぶんスッキリしましたね!

E2Eテストのベストプラクティス

E2Eテストを効果的に運用するためのポイントをまとめます。

1. 重要なユーザーフローに焦点を当てる

すべての操作をE2Eテストでカバーするのは現実的ではありません。重要度の高いフローに絞りましょう。

優先的にテストすべきフロー

  • ユーザー登録・ログイン
  • 商品購入・決済
  • データの保存・更新
  • 重要な業務プロセス

ユニットテストに任せるべきこと

  • 細かいバリデーションロジック
  • エッジケースの処理
  • 単純な計算処理

2. テストデータを適切に管理する

テストの独立性を保つため、データ管理は重要です。

// 各テストで新しいユーザーを作成
test.beforeEach(async ({ page }) => {
  // ユニークなメールアドレスを生成
  const timestamp = Date.now();
  const testUser = {
    email: `test-${timestamp}@example.com`,
    password: 'TestPassword123!'
  };
  
  // テストユーザーを作成
  await createTestUser(testUser);
  
  // ログイン
  await page.goto('/login');
  await page.fill('#email', testUser.email);
  await page.fill('#password', testUser.password);
  await page.click('#login-button');
});

// テスト後にクリーンアップ
test.afterEach(async () => {
  await cleanupTestUsers();
});

3. 適切な待機処理を行う

E2Eテストで最も多い問題の一つが、タイミングの問題です。

// ❌ 悪い例:固定時間の待機
await page.click('#submit');
await page.waitForTimeout(3000); // 3秒待つ - 不安定!

// ✅ 良い例:条件を満たすまで待機
await page.click('#submit');
await page.waitForSelector('#success-message', { 
  state: 'visible',
  timeout: 10000 // 最大10秒待つ
});

4. セレクタは安定したものを使う

要素を特定するセレクタは、変更に強いものを選びましょう。

// ❌ 脆弱:CSSクラスに依存(スタイル変更で壊れる)
await page.click('.btn.btn-primary.btn-large');

// ❌ 脆弱:HTMLの構造に依存
await page.click('div > div > button:nth-child(3)');

// ✅ 安定:data属性やID、テスト用の属性を使う
await page.click('[data-testid="submit-button"]');
await page.click('#submit-button');

// ✅ 安定:テキストコンテンツ(変更の少ないもの)
await page.click('button:has-text("送信")');

HTML側でも、テスト用の属性を用意すると良いでしょう:

<!-- テスト用の属性を追加 -->
<button 
  id="submit-button"
  data-testid="submit-button"
  class="btn btn-primary">
  送信
</button>

5. テストを並列実行できるように設計する

テストは互いに独立していれば、並列実行で時間を短縮できます。

// playwright.config.js
export default {
  // 並列実行するワーカー数
  workers: 4,
  
  // 各テストがタイムアウトする時間
  timeout: 30000,
  
  use: {
    // 各テストで新しいブラウザコンテキストを使用
    browserName: 'chromium',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
  },
};

6. 失敗時のデバッグ情報を記録する

テストが失敗した時に、原因を特定しやすくしましょう。

test('重要な機能テスト', async ({ page }) => {
  try {
    // テスト内容
    await page.goto('/important-feature');
    await page.click('#critical-button');
    
  } catch (error) {
    // 失敗時にスクリーンショットを保存
    await page.screenshot({ 
      path: `failures/test-${Date.now()}.png`,
      fullPage: true 
    });
    
    // HTMLも保存
    const html = await page.content();
    await fs.writeFile(`failures/test-${Date.now()}.html`, html);
    
    // エラーを再スロー
    throw error;
  }
});

7. テストの可読性を高める

テストは「何をテストしているか」が明確にわかるように書きましょう。

// ❌ 読みにくい
test('test1', async ({ page }) => {
  await page.goto('/');
  await page.click('#btn1');
  await page.fill('#input1', 'test');
  await page.click('#btn2');
  expect(await page.locator('#msg').textContent()).toBe('Success');
});

// ✅ 読みやすい
test('ユーザーがプロフィールを更新できる', async ({ page }) => {
  // 準備:プロフィールページに移動
  await page.goto('/profile');
  
  // 実行:プロフィール編集ボタンをクリック
  await page.click('[data-testid="edit-profile-button"]');
  
  // 実行:名前を変更
  await page.fill('[data-testid="name-input"]', '新しい名前');
  
  // 実行:保存ボタンをクリック
  await page.click('[data-testid="save-button"]');
  
  // 検証:成功メッセージが表示される
  const successMessage = page.locator('[data-testid="success-message"]');
  await expect(successMessage).toHaveText('プロフィールを更新しました');
});

8. API呼び出しをモックする(必要に応じて)

外部APIへの依存を減らすことで、テストを安定させることができます。

test('天気情報を表示する', async ({ page }) => {
  // 外部天気APIのレスポンスをモック
  await page.route('**/api/weather/*', route => {
    route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify({
        temperature: 25,
        condition: '晴れ',
        humidity: 60
      })
    });
  });

  await page.goto('/weather');
  
  await expect(page.locator('#temperature')).toHaveText('25°C');
  await expect(page.locator('#condition')).toHaveText('晴れ');
});

E2Eテストの課題と対処法

E2Eテストにも課題があります。よくある問題とその対処法を見ていきましょう。

課題1:実行時間が長い

E2Eテストは実際のブラウザを動かすため、時間がかかります。

対処法

  • 並列実行を活用する
  • CI/CDで自動化し、開発中は必要なテストだけ実行
  • クリティカルなテストとそうでないテストを分ける
// 重要なテストにタグを付ける
test('【重要】ユーザー登録', { tag: '@critical' }, async ({ page }) => {
  // ...
});

test('【通常】プロフィール編集', async ({ page }) => {
  // ...
});
# 重要なテストのみ実行
npx playwright test --grep @critical

課題2:テストの不安定性(フレイキネス)

時々成功したり失敗したりする「不安定なテスト」は、信頼性を損ないます。

原因

  • タイミングの問題
  • ネットワークの遅延
  • 外部サービスの不安定性
  • テスト間の依存関係

対処法

  • 適切な待機処理を使う
  • テストの独立性を保つ
  • リトライ機能を活用
// playwright.config.js
export default {
  // 失敗したテストを自動的に2回までリトライ
  retries: 2,
  
  use: {
    // 各操作のタイムアウトを設定
    actionTimeout: 10000,
    navigationTimeout: 30000,
  },
};

課題3:メンテナンスコストが高い

UIの変更によってテストが壊れやすくなります。

対処法

  • ページオブジェクトパターンを使う
  • 安定したセレクタを使う
  • テストを定期的にリファクタリング
// ページオブジェクトで変更箇所を一箇所に集約
class LoginPage {
  constructor(page) {
    this.page = page;
    // セレクタを一箇所で管理
    this.emailInput = '#email'; // UIが変わってもここだけ修正
    this.passwordInput = '#password';
    this.submitButton = '#login-button';
  }

  async login(email, password) {
    await this.page.fill(this.emailInput, email);
    await this.page.fill(this.passwordInput, password);
    await this.page.click(this.submitButton);
  }
}

課題4:初期セットアップが複雑

環境構築やCI/CD連携が難しい場合があります。

対処法

  • 公式ドキュメントを参考にする
  • 設定ファイルをチームで共有
  • Docker等で環境を統一
# .github/workflows/e2e-tests.yml
name: E2E Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '18'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Install Playwright Browsers
        run: npx playwright install --with-deps
      
      - name: Run E2E tests
        run: npx playwright test
      
      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v3
        with:
          name: playwright-report
          path: playwright-report/

E2Eテストとユニットテストの使い分け

「どこまでE2Eテストでカバーして、どこまでユニットテストに任せるべきか?」という question はよく聞かれます。

テストピラミッド

理想的なテスト構成は「テストピラミッド」と呼ばれる形です:

        ┌─────┐
        │ E2E │ ← 少数の重要なフロー
        ├─────┤
        │統合 │ ← APIや連携部分
      ┌─┴─────┴─┐
      │ユニット │ ← 大部分のロジック
      └─────────┘

ユニットテスト(土台)

  • 数が多い
  • 実行が速い
  • 細かい条件分岐を網羅

統合テスト(中間)

  • 適度な数
  • モジュール間の連携を確認

E2Eテスト(頂点)

  • 数は少なめ
  • 重要なユーザーストーリーのみ
  • 全体の動作を確認

具体例:フォームバリデーション

ユニットテストでカバー

// バリデーション関数のテスト
describe('validateEmail', () => {
  test('正しいメールアドレスの場合、trueを返す', () => {
    expect(validateEmail('test@example.com')).toBe(true);
  });

  test('@がない場合、falseを返す', () => {
    expect(validateEmail('testexample.com')).toBe(false);
  });

  test('ドメインがない場合、falseを返す', () => {
    expect(validateEmail('test@')).toBe(false);
  });
  
  // その他、様々なエッジケース
});

E2Eテストでカバー

// 実際のUでの動作確認
test('無効なメールアドレスを入力すると、エラーメッセージが表示される', async ({ page }) => {
  await page.goto('/register');
  await page.fill('#email', 'invalid-email');
  await page.click('#submit');
  
  // エラーメッセージの表示を確認
  await expect(page.locator('#email-error')).toHaveText('正しいメールアドレスを入力してください');
});

このように役割分担することで、効率的にテストを実施できます。

まとめ

E2E(End-to-End)テストは、アプリケーション全体を通してユーザーの視点で動作を検証するテスト手法です。

E2Eテストの重要ポイント

E2Eテストとは

  • ユーザーの操作フロー全体をテストする
  • 実際のブラウザで動作を確認
  • ユニットテストでは見つけられない問題を発見

主要なツール

  • Playwright:モダン、高速、クロスブラウザ対応
  • Cypress:優れた開発者体験、デバッグしやすい
  • Selenium:歴史があり、多言語対応

ベストプラクティス

  1. 重要なユーザーフローに焦点を当てる
  2. 適切な待機処理を行う
  3. 安定したセレクタを使う
  4. ページオブジェクトパターンで保守性を高める
  5. テストの独立性を保つ
  6. 失敗時のデバッグ情報を記録する

よくある課題と対処法

  • 実行時間が長い → 並列実行、重要度による分類
  • テストが不安定 → リトライ、適切な待機処理
  • メンテナンスコスト → ページオブジェクト、安定したセレクタ

テストピラミッドを意識する

多くのユニットテスト(速い、細かい)
  ↓
適度な統合テスト(連携確認)
  ↓
少数のE2Eテスト(重要フロー)

始めるためのヒント

  1. 小さく始める:まずはログインフローなど、シンプルなものから
  2. 段階的に拡大:慣れてきたら、重要度の高いフローを追加
  3. チームで共有:テストのノウハウをドキュメント化
  4. 継続的に改善:不安定なテストは放置せず、改善する

E2Eテストは初期の学習コストこそありますが、一度仕組みを作れば、リリース前の不安を大きく減らしてくれます。手動での動作確認に時間を取られている方は、ぜひE2Eテストの導入を検討してみてください。

実際のユーザーが使う画面が自動でテストされる様子を見ると、「これは便利だ!」と実感できるはずです。

Happy Testing! 🎭✨

参考資料


この記事が少しでも参考になれば嬉しいです。E2Eテストについての質問やフィードバックがあれば、ぜひコメントで教えてください!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?