自動テストチュートリアル

Playwright、Seleniumなどの自動テストツールを学ぼう

アサーション(検証)

アサーション(検証)

アサーション(assertion)は、テストにおける「検証」の部分です。期待する結果と実際の結果を比較し、テストの合否を判定します。このチャプターでは、アサーションの基本から、様々な検証方法、良いアサーションの書き方まで学びます。

アサーションとは?

基本概念

アサーションは、「期待する状態」と「実際の状態」が一致しているかを確認するための仕組みです。

// 基本的なアサーションの形
await expect(実際の値).toBe(期待する値);

アサーションが失敗すると、テストは失敗し、詳細なエラーメッセージが表示されます。

アサーションの役割

役割 説明
検証 期待通りの動作をしているか確認
ドキュメント コードの期待動作を明示
早期発見 バグを早い段階で検出
信頼性 テストの信頼性を確保

Playwright の expect 文

Playwright では、expect 関数を使用してアサーションを記述します。

基本的な使い方

import { test, expect } from '@playwright/test';

test('基本的なアサーション', async ({ page }) => {
  await page.goto('https://example.com');

  // ページタイトルの検証
  await expect(page).toHaveTitle('Example Domain');

  // URLの検証
  await expect(page).toHaveURL('https://example.com/');

  // 要素の表示検証
  await expect(page.locator('h1')).toBeVisible();

  // テキスト内容の検証
  await expect(page.locator('h1')).toHaveText('Example Domain');
});

[!INFO] 自動待機機能

Playwright の expect は、アサーションが成功するまで自動的に待機します(デフォルト5秒)。これにより、非同期処理の完了を待つコードを書く必要が減ります。

様々な検証方法

1. 要素の表示・非表示

test('要素の表示状態を検証', async ({ page }) => {
  await page.goto('https://example.com/login');

  // 要素が表示されている
  await expect(page.locator('#login-form')).toBeVisible();

  // 要素が非表示
  await expect(page.locator('#error-message')).not.toBeVisible();

  // 要素が存在しない
  await expect(page.locator('#deleted-item')).toHaveCount(0);

  // 要素が有効
  await expect(page.locator('#submit-button')).toBeEnabled();

  // 要素が無効
  await expect(page.locator('#submit-button')).toBeDisabled();
});

2. テキストの検証

test('テキスト内容を検証', async ({ page }) => {
  await page.goto('https://example.com/products');

  // 完全一致
  await expect(page.locator('h1')).toHaveText('商品一覧');

  // 部分一致
  await expect(page.locator('.description')).toContainText('ノートPC');

  // 正規表現
  await expect(page.locator('.price')).toHaveText(/¥[0-9,]+/);

  // 複数要素のテキスト配列
  await expect(page.locator('.product-name')).toHaveText([
    'ノートPC',
    'マウス',
    'キーボード'
  ]);
});

3. 属性の検証

test('要素の属性を検証', async ({ page }) => {
  await page.goto('https://example.com/form');

  // 属性の値
  await expect(page.locator('#email')).toHaveAttribute('type', 'email');

  // 属性の存在
  await expect(page.locator('#required-field')).toHaveAttribute('required');

  // クラスの存在
  await expect(page.locator('#active-tab')).toHaveClass(/active/);

  // 複数のクラス
  await expect(page.locator('#button')).toHaveClass('btn btn-primary btn-large');
});

4. 入力値の検証

test('フォーム入力値を検証', async ({ page }) => {
  await page.goto('https://example.com/form');

  // 入力フィールドの値
  await page.fill('#name', '山田太郎');
  await expect(page.locator('#name')).toHaveValue('山田太郎');

  // チェックボックス
  await page.check('#agree');
  await expect(page.locator('#agree')).toBeChecked();

  // ラジオボタン
  await page.check('#option-a');
  await expect(page.locator('#option-a')).toBeChecked();
  await expect(page.locator('#option-b')).not.toBeChecked();

  // セレクトボックス
  await page.selectOption('#country', 'japan');
  await expect(page.locator('#country')).toHaveValue('japan');
});

5. 要素数の検証

test('要素の数を検証', async ({ page }) => {
  await page.goto('https://example.com/products');

  // 要素数が指定した数
  await expect(page.locator('.product-card')).toHaveCount(10);

  // 要素が存在する(1つ以上)
  await expect(page.locator('.product-card')).not.toHaveCount(0);

  // 特定の条件に一致する要素数
  const saleItems = page.locator('.product-card:has(.sale-badge)');
  await expect(saleItems).toHaveCount(3);
});

6. URL とタイトルの検証

test('ページのURLとタイトルを検証', async ({ page }) => {
  await page.goto('https://example.com');

  // URLの完全一致
  await expect(page).toHaveURL('https://example.com/');

  // URLの部分一致(正規表現)
  await expect(page).toHaveURL(/.*example.com/);

  // タイトルの完全一致
  await expect(page).toHaveTitle('Example Domain');

  // タイトルの部分一致
  await expect(page).toHaveTitle(/Example/);

  // ページ遷移後のURL検証
  await page.click('#login-button');
  await expect(page).toHaveURL(/.*dashboard/);
});

7. CSS とスタイルの検証

test('CSSとスタイルを検証', async ({ page }) => {
  await page.goto('https://example.com');

  // CSSプロパティの値
  await expect(page.locator('.header')).toHaveCSS('background-color', 'rgb(0, 0, 255)');

  // 要素の表示/非表示(visibility)
  await expect(page.locator('.hidden-element')).toHaveCSS('display', 'none');

  // フォントサイズ
  await expect(page.locator('h1')).toHaveCSS('font-size', '32px');
});

8. スクリーンショット比較

test('ビジュアルリグレッションテスト', async ({ page }) => {
  await page.goto('https://example.com');

  // 要素のスクリーンショット比較
  await expect(page.locator('.hero-section')).toHaveScreenshot('hero.png');

  // ページ全体のスクリーンショット比較
  await expect(page).toHaveScreenshot('full-page.png');

  // 許容誤差を指定
  await expect(page.locator('.dynamic-chart')).toHaveScreenshot('chart.png', {
    maxDiffPixels: 100
  });
});

カスタムアサーション

API レスポンスの検証

test('APIレスポンスを検証', async ({ request }) => {
  const response = await request.get('https://api.example.com/products');

  // ステータスコード
  expect(response.ok()).toBeTruthy();
  expect(response.status()).toBe(200);

  // レスポンスヘッダー
  const headers = response.headers();
  expect(headers['content-type']).toContain('application/json');

  // レスポンスボディ
  const body = await response.json();
  expect(body.products).toHaveLength(10);
  expect(body.products[0]).toHaveProperty('id');
  expect(body.products[0].name).toBe('ノートPC');
  expect(body.products[0].price).toBeGreaterThan(0);
});

JavaScript の値の検証

test('JavaScript値を検証', async ({ page }) => {
  await page.goto('https://example.com');

  // ページ内のJavaScript変数を取得して検証
  const cartCount = await page.evaluate(() => {
    return window.localStorage.getItem('cartCount');
  });
  expect(cartCount).toBe('3');

  // 配列の検証
  const items = ['りんご', 'バナナ', 'オレンジ'];
  expect(items).toHaveLength(3);
  expect(items).toContain('バナナ');

  // オブジェクトの検証
  const user = { name: '山田太郎', age: 30 };
  expect(user).toHaveProperty('name');
  expect(user.age).toBeGreaterThanOrEqual(18);
});

良いアサーションの書き方

1. 明確で具体的な検証

// ❌ 悪い例:漠然とした検証
test('ログイン', async ({ page }) => {
  await page.goto('https://example.com/login');
  await page.fill('#email', 'user@example.com');
  await page.fill('#password', 'password');
  await page.click('button');
  await expect(page.locator('div')).toBeVisible(); // 何を検証しているのか不明
});

// ✅ 良い例:具体的な検証
test('ログイン成功時にダッシュボードが表示される', async ({ page }) => {
  await page.goto('https://example.com/login');
  await page.fill('#email', 'user@example.com');
  await page.fill('#password', 'password123');
  await page.click('button[type="submit"]');

  // 具体的に何を検証しているか明確
  await expect(page).toHaveURL(/.*dashboard/);
  await expect(page.locator('h1')).toContainText('ダッシュボード');
  await expect(page.locator('#user-name')).toContainText('user@example.com');
});

2. 適切な待機

// ❌ 悪い例:固定の待機時間
test('検索結果が表示される', async ({ page }) => {
  await page.goto('https://example.com');
  await page.fill('#search', 'ノートPC');
  await page.click('#search-button');
  await page.waitForTimeout(3000); // 固定待機は不安定
  await expect(page.locator('.results')).toBeVisible();
});

// ✅ 良い例:条件待機
test('検索結果が表示される', async ({ page }) => {
  await page.goto('https://example.com');
  await page.fill('#search', 'ノートPC');
  await page.click('#search-button');

  // 自動待機(expect が自動的に待つ)
  await expect(page.locator('.results')).toBeVisible();
});

3. 複数の観点から検証

test('フォーム送信成功を多角的に検証', async ({ page }) => {
  await page.goto('https://example.com/contact');
  await page.fill('#name', '山田太郎');
  await page.fill('#email', 'yamada@example.com');
  await page.fill('#message', 'お問い合わせ内容');
  await page.click('#submit');

  // 複数の観点で検証(より確実)
  await expect(page.locator('.success-message')).toBeVisible();
  await expect(page.locator('.success-message')).toContainText('送信完了');
  await expect(page).toHaveURL(/.*success/);
  await expect(page.locator('#name')).toHaveValue(''); // フォームがリセットされている
});

4. エラーメッセージを含める

test('商品価格が正しいことを確認', async ({ page }) => {
  await page.goto('https://example.com/products/123');

  const price = await page.locator('.product-price').textContent();

  // カスタムエラーメッセージ
  expect(price, '商品価格が期待値と異なります').toBe('¥100,000');
});

5. ソフトアサーション

テストを途中で止めず、すべての検証を実行したい場合:

import { test, expect } from '@playwright/test';

test('複数項目を検証(ソフトアサーション)', async ({ page }) => {
  await page.goto('https://example.com/products/123');

  // すべての検証を実行(途中で失敗しても続行)
  await expect.soft(page.locator('.product-name')).toContainText('ノートPC');
  await expect.soft(page.locator('.product-price')).toContainText('¥100,000');
  await expect.soft(page.locator('.product-stock')).toContainText('在庫あり');
  await expect.soft(page.locator('.product-rating')).toContainText('★4.5');

  // 最後にすべてのソフトアサーションの結果を確認
});

アサーションのパターン集

パターン1: 存在確認

test('要素の存在確認パターン', async ({ page }) => {
  await page.goto('https://example.com');

  // 要素が存在する
  await expect(page.locator('#header')).toBeVisible();

  // 要素が存在しない
  await expect(page.locator('#deleted-banner')).not.toBeVisible();

  // 条件付き要素の存在
  const errorMessage = page.locator('.error');
  const count = await errorMessage.count();
  if (count > 0) {
    await expect(errorMessage).toBeVisible();
  }
});

パターン2: リスト検証

test('リスト要素の検証パターン', async ({ page }) => {
  await page.goto('https://example.com/products');

  // リストの要素数
  await expect(page.locator('.product-card')).toHaveCount(10);

  // すべての要素に特定のクラスがある
  const products = page.locator('.product-card');
  const count = await products.count();
  for (let i = 0; i < count; i++) {
    await expect(products.nth(i)).toHaveClass(/product-card/);
  }

  // 特定の要素が含まれる
  await expect(page.locator('.product-name')).toContainText(['ノートPC', 'マウス']);
});

パターン3: 状態変化の検証

test('状態変化の検証パターン', async ({ page }) => {
  await page.goto('https://example.com/cart');

  // 初期状態
  await expect(page.locator('#cart-count')).toHaveText('0');
  await expect(page.locator('.empty-cart-message')).toBeVisible();

  // アクション実行
  await page.click('#add-item-1');

  // 変化後の状態
  await expect(page.locator('#cart-count')).toHaveText('1');
  await expect(page.locator('.empty-cart-message')).not.toBeVisible();
  await expect(page.locator('.cart-item')).toHaveCount(1);
});

[!TIP] アサーションの選択基準

  • 厳密な検証が必要: toHaveText(), toBe() を使用
  • 柔軟な検証: toContainText(), 正規表現を使用
  • 複数の可能性: toHaveText([...]) で配列を使用
  • 動的な値: 正規表現や toMatch() を使用

よくある間違いと対処法

間違い1: 過度な待機時間

// ❌ 避けるべき
await page.waitForTimeout(5000);
await expect(page.locator('.result')).toBeVisible();

// ✅ 推奨
await expect(page.locator('.result')).toBeVisible({ timeout: 5000 });

間違い2: 不安定なセレクタ

// ❌ 避けるべき(構造変更に弱い)
await expect(page.locator('div > div > span:nth-child(3)')).toBeVisible();

// ✅ 推奨(意味のあるセレクタ)
await expect(page.locator('[data-testid="user-name"]')).toBeVisible();

間違い3: 検証なしのテスト

// ❌ 悪い例:検証がない
test('ログイン', async ({ page }) => {
  await page.goto('https://example.com/login');
  await page.fill('#email', 'user@example.com');
  await page.fill('#password', 'password');
  await page.click('button');
  // 検証がない!
});

// ✅ 良い例:適切な検証がある
test('ログイン成功', async ({ page }) => {
  await page.goto('https://example.com/login');
  await page.fill('#email', 'user@example.com');
  await page.fill('#password', 'password');
  await page.click('button[type="submit"]');

  // 適切な検証
  await expect(page).toHaveURL(/.*dashboard/);
  await expect(page.locator('#welcome-message')).toBeVisible();
});

まとめ

  • アサーションは、期待する結果と実際の結果を比較する検証機構
  • Playwright の expect自動待機機能を持ち、非同期処理に強い
  • 様々な検証方法がある:表示、テキスト、属性、値、数、URL、CSS など
  • 良いアサーションは明確、具体的、適切な待機を持つ
  • 複数の観点から検証することで、テストの信頼性が向上
  • ソフトアサーションを使うと、すべての検証を実行できる
  • 意味のあるセレクタと適切な待機戦略が重要

次のチャプターでは、テスト設計の基本について学びます。