T-CREATOR

Playwright でマルチユーザー・認証テストを実現する

Playwright でマルチユーザー・認証テストを実現する

Web アプリケーションの複雑化に伴い、複数のユーザーが同時にアクセスする環境での認証テストが重要になっています。異なる権限を持つユーザーが同じシステムを利用する際の動作確認や、セッション管理の検証など、マルチユーザー環境特有の課題に対応するテストが求められます。

Playwright は、その強力なブラウザコンテキスト分離機能により、これらの複雑な認証シナリオを効率的にテストできる優れたツールです。本記事では、Playwright を使用してマルチユーザー認証テストを実現する方法を、実装例を交えながら詳しく解説いたします。

背景

現代 Web アプリケーションの認証複雑化

現代の Web アプリケーションでは、シンプルなユーザー名・パスワード認証から、より高度な認証方式へと進化しています。多要素認証(MFA)、ソーシャルログイン、トークンベース認証など、様々な認証手法が組み合わせて使用されるようになりました。

さらに、アプリケーション内では管理者、一般ユーザー、ゲストユーザーなど、複数の権限レベルが存在し、それぞれ異なる機能へのアクセス権限を持っています。このような環境では、単一ユーザーでのテストだけでは不十分になっています。

従来のテスト手法の限界

従来の E2E テストでは、テストケースごとにブラウザを完全にリセットし、毎回ログイン処理を実行する手法が一般的でした。しかし、この方法には以下のような制約がありました。

テスト実行時間の増大、認証状態の管理困難、複数ユーザーの同時操作テスト不可、セッション間での影響確認不可といった問題が発生していました。

マルチユーザーシナリオの必要性

実際のユーザー利用シナリオを考えると、以下のような状況でのテストが必要です。

管理者が設定変更を行っている最中に一般ユーザーがシステムにアクセスする場合、複数の一般ユーザーが同じリソースに対して操作を行う場合、権限レベルの異なるユーザーが同じ画面を表示した際の表示内容の違い確認などです。

これらのシナリオを手動でテストするのは時間がかかり、また人的ミスが発生しやすいという課題もありました。

課題

複数ユーザーの状態管理

マルチユーザーテストでの最大の課題は、複数のユーザー状態を同時に管理することです。各ユーザーは異なる認証情報、セッション状態、権限レベルを持っており、これらの状態を適切に分離して管理する必要があります。

従来のテストツールでは、グローバルな状態が共有されてしまい、あるユーザーの操作が他のユーザーのテストに影響を与えてしまう問題がありました。

セッション分離の困難さ

Web ブラウザのセッション管理は複雑で、Cookie、LocalStorage、SessionStorage など複数の仕組みが組み合わされています。これらの状態を完全に分離し、各ユーザーが独立したセッションを持つようにテストを設計することは技術的に困難でした。

また、シークレットブラウザモードを使用しても、完全な分離を実現するには追加の設定や工夫が必要になります。

認証フローの複雑さ

現代の認証フローは多段階になっており、ログイン→多要素認証→権限確認→リダイレクトといった複数のステップを含みます。これらの各ステップでエラーが発生する可能性があり、すべてのパターンをテストするには膨大な時間と労力が必要です。

さらに、認証トークンの有効期限、リフレッシュトークンの処理、ログアウト時のクリーンアップなど、状態管理も複雑になっています。

テスト環境の構築

マルチユーザーテストを実行するためには、複数のテストユーザーアカウント、適切なテストデータ、分離されたテスト環境が必要です。これらの環境を構築し、維持することは開発チームにとって大きな負担となっていました。

また、CI/CD パイプラインでの自動実行を考慮すると、環境の再現性や実行時間の最適化も重要な課題となります。

解決策

Playwright のコンテキスト分離機能

Playwright の最大の特徴は、Browser Context という仕組みによる完全なセッション分離機能です。この機能により、同一ブラウザインスタンス内で複数の独立したセッションを作成できます。

以下の図は、Playwright のコンテキスト分離の仕組みを示しています。

mermaidflowchart TD
    browser[ブラウザインスタンス] --> ctx1[Context 1<br/>管理者ユーザー]
    browser --> ctx2[Context 2<br/>一般ユーザー]
    browser --> ctx3[Context 3<br/>ゲストユーザー]
    
    ctx1 --> page1[Page 1<br/>管理画面]
    ctx1 --> page2[Page 2<br/>設定画面]
    
    ctx2 --> page3[Page 3<br/>ダッシュボード]
    
    ctx3 --> page4[Page 4<br/>ランディングページ]
    
    style ctx1 fill:#e1f5fe
    style ctx2 fill:#f3e5f5
    style ctx3 fill:#e8f5e8

各コンテキストは完全に独立しており、Cookie、LocalStorage、認証状態などがすべて分離されます。これにより、複数ユーザーの同時操作を安全にテストできます。

認証状態の永続化

Playwright では、認証済みの状態をファイルに保存し、後続のテストで再利用する機能を提供しています。これにより、毎回ログイン処理を実行する必要がなくなり、テスト実行時間を大幅に短縮できます。

認証状態の永続化により、テストの実行速度が向上し、認証フローに依存しない部分のテストに集中できるようになります。

並列テスト実行

Playwright は設計段階から並列実行を考慮して作られており、複数のテストを同時に実行できます。マルチユーザーシナリオでは、この並列実行機能を活用して、異なるユーザーの操作を同時に実行し、相互の影響を検証できます。

並列実行時も各テストは独立したコンテキストで実行されるため、テスト間での干渉を心配する必要がありません。

ユーザーセッション管理

効率的なマルチユーザーテストには、ユーザーセッションの適切な管理が欠かせません。Playwright では、事前に複数のユーザーセッションを準備し、テスト実行時に必要に応じて切り替える仕組みを構築できます。

具体例

基本的な認証テスト設定

まず、Playwright でマルチユーザー認証テストを行うための基本設定から始めましょう。プロジェクトの初期設定とテスト環境の構築方法を説明します。

プロジェクト設定

typescript// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: 'html',

テスト実行の並列性を有効にし、CI 環境での安定性を確保する設定を含めています。

ユーザー定義とセットアップ

typescript// tests/auth.setup.ts
import { test as setup, expect } from '@playwright/test';

// 複数のユーザーを定義
const users = {
  admin: { email: 'admin@example.com', password: 'admin123' },
  user: { email: 'user@example.com', password: 'user123' },
  guest: { email: 'guest@example.com', password: 'guest123' }
};

各ユーザーの認証情報を管理し、テスト間で一貫性を保ちます。

認証状態の保存

typescript// セットアップファイルの続き
setup('authenticate as admin', async ({ page }) => {
  await page.goto('/login');
  await page.fill('[data-testid="email"]', users.admin.email);
  await page.fill('[data-testid="password"]', users.admin.password);
  await page.click('[data-testid="login-button"]');
  
  // 認証成功を確認
  await expect(page.locator('[data-testid="user-menu"]')).toBeVisible();
  
  // 認証状態を保存
  await page.context().storageState({ 
    path: 'tests/auth/admin-auth.json' 
  });
});

認証処理を一度実行し、その状態を JSON ファイルに保存します。これにより、後続のテストで認証状態を再利用できます。

複数ユーザーの同時テスト

複数のユーザーが同時にシステムを利用するシナリオをテストする方法を説明します。

並列ユーザーテストの実装

typescript// tests/multi-user.spec.ts
import { test, expect } from '@playwright/test';

test.describe('マルチユーザー同時操作テスト', () => {
  test('管理者と一般ユーザーの同時アクセス', async ({ browser }) => {
    // 管理者コンテキストの作成
    const adminContext = await browser.newContext({
      storageState: 'tests/auth/admin-auth.json'
    });
    const adminPage = await adminContext.newPage();
    
    // 一般ユーザーコンテキストの作成
    const userContext = await browser.newContext({
      storageState: 'tests/auth/user-auth.json'
    });
    const userPage = await userContext.newPage();

それぞれ独立したブラウザコンテキストを作成し、保存済みの認証状態を読み込みます。

同時操作の検証

typescript    // 両ユーザーが同じリソースにアクセス
    await Promise.all([
      adminPage.goto('/dashboard'),
      userPage.goto('/dashboard')
    ]);
    
    // 管理者は全ての機能が表示される
    await expect(adminPage.locator('[data-testid="admin-panel"]')).toBeVisible();
    
    // 一般ユーザーは制限された機能のみ表示
    await expect(userPage.locator('[data-testid="admin-panel"]')).not.toBeVisible();
    await expect(userPage.locator('[data-testid="user-dashboard"]')).toBeVisible();

Promise.all を使用して並列実行し、各ユーザーの表示内容が適切に制御されているかを確認します。

権限別テストシナリオ

権限レベルごとに異なるテストシナリオを実装する方法を説明します。

権限マトリクステスト

typescript// tests/permissions.spec.ts
const permissions = [
  { role: 'admin', canEdit: true, canDelete: true, canView: true },
  { role: 'editor', canEdit: true, canDelete: false, canView: true },
  { role: 'viewer', canEdit: false, canDelete: false, canView: true }
];

permissions.forEach(({ role, canEdit, canDelete, canView }) => {
  test(`${role}の権限テスト`, async ({ browser }) => {
    const context = await browser.newContext({
      storageState: `tests/auth/${role}-auth.json`
    });
    const page = await context.newPage();

各権限レベルでのアクセス制御をマトリクス形式でテストします。

権限チェックの実装

typescript    await page.goto('/admin/users');
    
    // 編集権限の確認
    if (canEdit) {
      await expect(page.locator('[data-testid="edit-button"]')).toBeVisible();
    } else {
      await expect(page.locator('[data-testid="edit-button"]')).not.toBeVisible();
    }
    
    // 削除権限の確認
    if (canDelete) {
      await expect(page.locator('[data-testid="delete-button"]')).toBeVisible();
    } else {
      await expect(page.locator('[data-testid="delete-button"]')).not.toBeVisible();
    }
  });
});

条件分岐を使用して、各権限レベルで表示されるべき要素が適切に制御されているかを検証します。

セッション永続化の実装

認証状態を効率的に管理し、テスト実行時間を最適化する方法を説明します。

グローバルセットアップの活用

typescript// global.setup.ts
import { chromium, FullConfig } from '@playwright/test';

async function globalSetup(config: FullConfig) {
  const browser = await chromium.launch();
  
  // 各ユーザーの認証状態を事前に作成
  for (const [role, credentials] of Object.entries(users)) {
    const context = await browser.newContext();
    const page = await context.newPage();
    
    await authenticateUser(page, credentials);
    await context.storageState({ 
      path: `tests/auth/${role}-auth.json` 
    });
    
    await context.close();
  }
  
  await browser.close();
}

export default globalSetup;

テスト実行前に全ユーザーの認証状態を準備し、各テストで再利用できるようにします。

認証ヘルパー関数

typescript// utils/auth.ts
export async function authenticateUser(page: Page, credentials: UserCredentials) {
  await page.goto('/login');
  await page.fill('[data-testid="email"]', credentials.email);
  await page.fill('[data-testid="password"]', credentials.password);
  await page.click('[data-testid="login-button"]');
  
  // MFA が必要な場合の処理
  if (credentials.totpSecret) {
    const totpCode = generateTOTP(credentials.totpSecret);
    await page.fill('[data-testid="totp-code"]', totpCode);
    await page.click('[data-testid="verify-button"]');
  }
  
  // 認証成功まで待機
  await page.waitForURL('/dashboard');
}

認証処理を共通化し、多要素認証にも対応できるヘルパー関数を実装します。

高度な認証シナリオ

typescript// tests/advanced-auth.spec.ts
test('リアルタイム同時操作テスト', async ({ browser }) => {
  // 3つの異なるユーザーコンテキストを作成
  const adminContext = await browser.newContext({
    storageState: 'tests/auth/admin-auth.json'
  });
  const user1Context = await browser.newContext({
    storageState: 'tests/auth/user1-auth.json'
  });
  const user2Context = await browser.newContext({
    storageState: 'tests/auth/user2-auth.json'
  });

  const adminPage = await adminContext.newPage();
  const user1Page = await user1Context.newPage();
  const user2Page = await user2Context.newPage();

異なる権限を持つ3つのユーザーで同時操作テストを実行する環境を構築します。

競合状態のテスト

typescript  // 同じドキュメントに同時編集を試行
  await Promise.all([
    adminPage.goto('/documents/1/edit'),
    user1Page.goto('/documents/1/edit'),
    user2Page.goto('/documents/1/edit')
  ]);

  // 管理者は編集可能
  await expect(adminPage.locator('[data-testid="edit-form"]')).toBeVisible();
  
  // 一般ユーザーは読み取り専用
  await expect(user1Page.locator('[data-testid="readonly-notice"]')).toBeVisible();
  await expect(user2Page.locator('[data-testid="readonly-notice"]')).toBeVisible();
  
  // クリーンアップ
  await Promise.all([
    adminContext.close(),
    user1Context.close(),
    user2Context.close()
  ]);
});

同時編集時の権限制御と競合状態の適切な処理を検証します。

以下の図は、マルチユーザーテストのフローを示しています。

mermaidsequenceDiagram
    participant Admin as 管理者
    participant User1 as ユーザー1
    participant User2 as ユーザー2
    participant App as アプリケーション
    participant DB as データベース

    Admin->>App: ログイン要求
    App->>Admin: 管理者権限で認証成功
    
    User1->>App: ログイン要求
    App->>User1: 一般ユーザー権限で認証成功
    
    User2->>App: ログイン要求
    App->>User2: 一般ユーザー権限で認証成功
    
    Admin->>App: ドキュメント編集要求
    App->>DB: 編集権限確認
    DB->>App: 許可
    App->>Admin: 編集画面表示
    
    User1->>App: 同一ドキュメント編集要求
    App->>DB: 編集権限確認
    DB->>App: 拒否
    App->>User1: 読み取り専用表示
    
    User2->>App: 同一ドキュメント編集要求
    App->>DB: 編集権限確認
    DB->>App: 拒否
    App->>User2: 読み取り専用表示

このシーケンス図では、複数ユーザーが同時に同一リソースにアクセスした際の権限制御フローを表示しています。管理者のみが編集権限を持ち、一般ユーザーは読み取り専用でアクセスする様子がわかります。

セッション管理の実装

typescript// utils/session-manager.ts
export class SessionManager {
  private contexts: Map<string, BrowserContext> = new Map();
  
  async createUserSession(browser: Browser, role: string): Promise<BrowserContext> {
    const context = await browser.newContext({
      storageState: `tests/auth/${role}-auth.json`
    });
    
    this.contexts.set(role, context);
    return context;
  }
  
  async switchUser(role: string): Promise<BrowserContext> {
    const context = this.contexts.get(role);
    if (!context) {
      throw new Error(`セッション ${role} が見つかりません`);
    }
    return context;
  }
  
  async cleanup(): Promise<void> {
    for (const context of this.contexts.values()) {
      await context.close();
    }
    this.contexts.clear();
  }
}

セッション管理を効率化するクラスを実装し、複数のユーザーコンテキストを統一的に管理できます。

まとめ

Playwright を使用したマルチユーザー認証テストは、現代の Web アプリケーション開発において必須のテスト手法となっています。Browser Context による完全なセッション分離機能により、複雑な認証シナリオも効率的にテストできるようになりました。

実装のポイント

#項目内容
1コンテキスト分離各ユーザーの状態を完全に独立して管理
2認証状態永続化ログイン処理の実行時間を大幅短縮
3並列実行複数ユーザーの同時操作を効率的にテスト
4権限マトリクスすべての権限パターンを網羅的に検証
5セッション管理再利用可能なセッション管理クラスの実装

認証状態の永続化により、テスト実行時間が従来の手法と比較して70%以上短縮される場合もあります。また、並列実行により、複雑なマルチユーザーシナリオも現実的な時間でテストできるようになりました。

継続的なテスト実行のためには、CI/CD パイプラインへの組み込みと、テストデータの適切な管理が重要です。Playwright の強力な機能を活用して、信頼性の高い認証テストを構築してください。

関連リンク