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?

実務1年目駆け出しエンジニアがLaravel&ReactでWebアプリケーション開発に挑戦してみた!(テスト・デバッグ編⑧)~E2Eテスト1[Playwrightセットアップ]~

0
Posted at

実務1年目駆け出しエンジニアがLaravel&ReactでWebアプリケーション開発に挑戦してみた!(その50)

0. 初めに

こんにちは!
このシリーズでは、見習いエンジニアの僕がWebアプリケーションをゼロから完成させるまでの軌跡をお届けしています。

1か月以上空いてしまって大変申し訳ございません。
モチベーションが下がってしまっていたことと、おまけコーナーを作るのに手こずってしまったのが原因です。

前回までで、Unitテスト、Featureテストが完了しておりました!

今日からは、いよいよ最後となるE2Eテストのテストケース作成・実行に取り組みます...!!

1. ブランチ運用

今日は、E2Eテストツールとして、Playwrightをインストール・セットアップしたいと思います。

そのため、developブランチを最新にして、test/e2e/set-upという新規ブランチを切って作業をしましょう。

なお、前回使った、test/feature/remainingブランチは不要なので削除しておきましょう。

2. インストール

必要なものをインストールします。

2.1 Playwright

まずは、Playwrightをインストールしましょう。

実行コマンド

/project-root/src
$ npm install --save-dev @playwright/test

2.2 Chromium

E2Eテストで実際に画面を動かすために使うブラウザとして今回は、Chromiumをインストールします。

実行コマンド

/project-root/src
$ npm install --save-dev @playwright/test

3. 設定ファイル作成

いくつかファイルを作成します。

3.1 playwright.config.js

srcディレクトリ直下に作成してください。

\project-root\src\playwright.config.js
/** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = {
  testDir: './e2e',
  globalSetup: './e2e/global-setup.js',
  fullyParallel: false,
  retries: 1,
  timeout: 30000,
  use: {
    baseURL: 'http://localhost',
    headless: true,
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
  },
  projects: [
    {
      name: 'chromium',
      use: { browserName: 'chromium' },
    },
  ],
  reporter: [['html', { open: 'never' }], ['list']],
};

export default config;

テストに必要な設定がいくつか書かれています。

詳しい解説は必要に応じて後からしていきますね。

3.2 /e2e/global-setup.js

\project-root\src\e2e\global-setup.js
import { request } from '@playwright/test';
import { execSync } from 'child_process';
import path from 'path';
import { fileURLToPath } from 'url';
import fs from 'fs';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const AUTH_DIR = path.join(__dirname, '.auth');

/** ユーザー種別ごとの認証情報 */
const USERS = {
  admin: {
    email: 'admin@example.com',
    password: 'password',
    authFile: path.join(AUTH_DIR, 'admin.json'),
  },
  user: {
    email: 'user@example.com',
    password: 'password',
    authFile: path.join(AUTH_DIR, 'user.json'),
  },
};

/** XSRF-TOKEN を取得してログインし、storageState を保存する */
async function saveAuthState(email, password, authFile) {
  const context = await request.newContext({ baseURL: 'http://localhost' });

  // GET / で XSRF-TOKEN クッキーを取得
  await context.get('/');

  const state = await context.storageState();
  const xsrfCookie = state.cookies.find((c) => c.name === 'XSRF-TOKEN');
  const xsrfToken = xsrfCookie ? decodeURIComponent(xsrfCookie.value) : '';

  const loginResponse = await context.post('/login', {
    headers: {
      'X-XSRF-TOKEN': xsrfToken,
      'Content-Type': 'application/x-www-form-urlencoded',
      Accept: 'text/html,application/xhtml+xml',
    },
    form: { email, password },
  });

  if (!loginResponse.ok() && loginResponse.status() !== 302) {
    throw new Error(
      `[global-setup] ログイン失敗 (${email}). Status: ${loginResponse.status()}`
    );
  }

  await context.storageState({ path: authFile });
  await context.dispose();
  console.log(`[global-setup] storageState 保存: ${authFile}`);
}

export default async function globalSetup() {
  // DB をリセットしてシードを実行
  console.log('\n[global-setup] Running migrate:fresh --seed ...');
  execSync('docker exec php-lab php artisan migrate:fresh --seed', {
    stdio: 'inherit',
  });

  // 一般ログインユーザー(固定メールアドレス)を作成
  execSync(
    `docker exec php-lab php artisan tinker --execute="\\App\\Models\\User::factory()->create(['email' => 'user@example.com', 'name' => 'テストユーザー', 'email_verified_at' => now()]);"
`,
    { stdio: 'inherit' }
  );

  // .auth ディレクトリを作成
  fs.mkdirSync(AUTH_DIR, { recursive: true });

  // 管理者・一般ユーザーそれぞれの認証状態を生成
  await saveAuthState(USERS.admin.email, USERS.admin.password, USERS.admin.authFile);
  await saveAuthState(USERS.user.email, USERS.user.password, USERS.user.authFile);

  console.log('[global-setup] 完了');
}

テスト実行の際に最初に設定される内容が書かれたファイルで、playwright.config.jsの以下の部分で登録済みです。

\project-root\src\playwright.config.js
globalSetup: './e2e/global-setup.js',

ここでは、認証情報やセキュリティ情報を保存する設定が書かれています。

詳しくは、次回以降解説していきます。

XSRFについては、今日のおまけコーナーで少し解説を入れますので、少々お待ちください。

3.3 /e2e/helpers/auth-paths.js

\project-root\src\e2e\helpers\auth-paths.js
/**
 * テストで使用する storageState ファイルのパス定義。
 *
 * 使い方:
 *   import { AUTH } from '../helpers/auth-paths.js';
 *
 *   // 認証なし(ゲスト)のテスト → test.use() 不要
 *
 *   // 一般ユーザーのテスト
 *   test.use({ storageState: AUTH.user });
 *
 *   // 管理ユーザーのテスト
 *   test.use({ storageState: AUTH.admin });
 */

/** @type {{ admin: string, user: string }} */
export const AUTH = {
    /** 管理者 (admin@example.com) */
    admin: './e2e/.auth/admin.json',
    /** 一般ユーザー (user@example.com) */
    user: './e2e/.auth/user.json',
};

認証情報の保存先のパスを毎回直書きするのは面倒なので作りました。

これを使うことで、ログアウトユーザー・ログインユーザー・管理ユーザーの3種類のテストを行うことができるようになります。

今日は使いませんが、次回以降使うので解説もその時にしますね。

3.4 .gitignore

# 既存...

# ↓追加
/playwright-report/
/test-results/
/e2e/.auth/

gitの追跡対象としないファイルやディレクトリを設定しているファイルです。
/playwright-report/はテストの結果を格納するもので、テストの実行者によって結果が変わってくるため(ブラウザの画面のスクリーンショットなどのファイルは容量が大きいためという理由もあります)、gitで管理するものとしては相性が悪いため追加しました。

/test-results/も同様です。

/e2e/.auth/は認証情報が含まれているため、セキュリティの観点からあまり望ましくないと考えて追加しました。

4. 動作確認

4.1 スモークテストケース作成

本格的なテストケースの作成は次回からとして、今日は動作確認用のスモークテストケースを作成したいと思います。

いわば、テストのテスト用ファイルで、電源を入れたときに煙が出ないかを確認する最低限の実験用ファイルです。

\project-root\src\e2e\smoke.spec.js
import { test, expect } from '@playwright/test';

// このファイルはゲスト(未認証)状態で実行される動作確認用スモークテスト

test.describe('スモークテスト: ホーム → 大学検索', () => {
  test('大学名を入力して検索ボタンを押すと、大学一覧ページに遷移する', async ({
    page,
  }) => {
      // 1. ホームページにアクセス(networkidle で React のレンダリング完了を待つ)
    await page.goto('/', { waitUntil: 'networkidle' });
    await expect(page).toHaveURL('/');

    // 2. 検索欄に文字を入力
    const searchInput = page.getByPlaceholder('大学名を入力...');
    await expect(searchInput).toBeVisible({ timeout: 10000 });
    await searchInput.fill('テスト');

    // 3. 検索ボタンを押下(type="submit" ボタン)
    await page.getByRole('button', { name: /検索/ }).click();

    // 4. 大学一覧ページに遷移したことを確認
    await expect(page).toHaveURL(new RegExp('/universities\\?query=' + encodeURIComponent('テスト')));
    await expect(page).toHaveTitle(/テスト.*大学一覧/);
  });

  test('検索結果に大学カードが表示される', async ({ page }) => {
    // シードデータに「テスト」を含む大学が存在するため結果が得られる
    await page.goto('/universities?query=テスト', { waitUntil: 'networkidle' });

    // 件数テキストが表示されること
    await expect(page.getByText(/件の検索結果/)).toBeVisible({ timeout: 10000 });

    // 少なくとも1件の大学カードが表示されること
    await expect(page.getByText(/テスト.+大学/).first()).toBeVisible();
  });

  test('空欄では検索ボタンが押せない', async ({ page }) => {
    await page.goto('/', { waitUntil: 'networkidle' });

    const submitButton = page.getByRole('button', { name: /検索/ });

    // 入力なしの状態では disabled になっていること
    await expect(submitButton).toBeDisabled({ timeout: 10000 });
  });
});

ファイルの命名規則として、.specというのを付けます。

4.2 ビルド

実行する前にソースコードをビルドしましょう。

ビルドって何?」って感じかもしれませんが(笑)。

ファイルを最適化・圧縮することです。

これにより、より本番に近い状態でテストをすることができます。

これについても今日のおまけコーナーで解説を入れます!

実行コマンド

/var/www
$ npm run build

実行が完了すると、/public/buildの下にいろいろ成果物が格納されます。

おそらく、TypeScriptを前提にしているのでエラーになるかもしれません。
その場合は、あらかじめ以下のファイルを修正してください。

\project-root\src\vite.config.js
export default defineConfig({
    plugins: [
        laravel({
            input: 'resources/js/app.jsx', // ←修正
            refresh: true,
        }),
        react(),
    ],

4.3 実行

準備がようやく整ったので、以下のコマンドで実行してみましょう。
実行コマンド

/project-root/src
$ npx playwright test e2e/smoke.spec.js

実行結果
image.png

また、実際のブラウザの動作を見ながら実行したいときは、以下です。

/project-root/src
$ npx playwright test e2e/smoke.spec.js

こんな感じでクリックや文字入力が自動的に行われているのをリアルタイムで見守りながらテスト結果を待つこともできます。
image.png

ここまでできたら、コミット・プッシュ、PR作成・マージ、ブランチ削除まで行いましょう。

5. おまけその1: XSRF(CSRF)について

こちらからどうぞ。
CSRF(XSRF)とは?

6. おまけその2: ビルドについて

こちらからどうぞ。
JavaScriptのビルドとは?【Web開発入門】

7. まとめ・次回予告

これまでの記事一覧

☆要件定義・設計編

☆環境構築編

☆バックエンド実装編

☆フロントエンド実装編

☆テスト・デバッグ編

軽く宣伝

YouTubeを始めました(というか始めてました)。
内容としては、Webエンジニアの生活や稼げるようになるまでの成長記録などを発信していく予定です。

現在、まったく再生されておらず、落ち込みそうなので、見てくださる方はぜひ高評価を教えもらえると励みになると思います。"(-""-)"

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?