実務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をインストールしましょう。
実行コマンド
$ npm install --save-dev @playwright/test
2.2 Chromium
E2Eテストで実際に画面を動かすために使うブラウザとして今回は、Chromiumをインストールします。
実行コマンド
$ npm install --save-dev @playwright/test
3. 設定ファイル作成
いくつかファイルを作成します。
3.1 playwright.config.js
srcディレクトリ直下に作成してください。
/** @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
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の以下の部分で登録済みです。
globalSetup: './e2e/global-setup.js',
ここでは、認証情報やセキュリティ情報を保存する設定が書かれています。
詳しくは、次回以降解説していきます。
XSRFについては、今日のおまけコーナーで少し解説を入れますので、少々お待ちください。
3.3 /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 スモークテストケース作成
本格的なテストケースの作成は次回からとして、今日は動作確認用のスモークテストケースを作成したいと思います。
いわば、テストのテスト用ファイルで、電源を入れたときに煙が出ないかを確認する最低限の実験用ファイルです。
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 ビルド
実行する前にソースコードをビルドしましょう。
「ビルドって何?」って感じかもしれませんが(笑)。
ファイルを最適化・圧縮することです。
これにより、より本番に近い状態でテストをすることができます。
これについても今日のおまけコーナーで解説を入れます!
実行コマンド
$ npm run build
実行が完了すると、/public/buildの下にいろいろ成果物が格納されます。
おそらく、TypeScriptを前提にしているのでエラーになるかもしれません。
その場合は、あらかじめ以下のファイルを修正してください。
export default defineConfig({
plugins: [
laravel({
input: 'resources/js/app.jsx', // ←修正
refresh: true,
}),
react(),
],
4.3 実行
準備がようやく整ったので、以下のコマンドで実行してみましょう。
実行コマンド
$ npx playwright test e2e/smoke.spec.js
また、実際のブラウザの動作を見ながら実行したいときは、以下です。
$ npx playwright test e2e/smoke.spec.js
こんな感じでクリックや文字入力が自動的に行われているのをリアルタイムで見守りながらテスト結果を待つこともできます。

ここまでできたら、コミット・プッシュ、PR作成・マージ、ブランチ削除まで行いましょう。
5. おまけその1: XSRF(CSRF)について
こちらからどうぞ。
CSRF(XSRF)とは?
6. おまけその2: ビルドについて
こちらからどうぞ。
JavaScriptのビルドとは?【Web開発入門】
7. まとめ・次回予告
これまでの記事一覧
☆要件定義・設計編
☆環境構築編
☆バックエンド実装編
- その7: バックエンド実装編① ~認証機能作成~
- その8: バックエンド実装編②前編 ~レビュー投稿機能作成~
- その8.5: バックエンド実装編②後編 ~レビュー投稿機能作成~
- その9: バックエンド実装編③ ~レビューCRUD機能作成~
- その10: バックエンド実装編④ ~レビューCRUD機能作成その2~
- その11: バックエンド実装編⑤ ~新規大学・学部・研究室作成機能作成~
- その12: バックエンド実装編⑥ ~大学検索機能作成~
- その13: バックエンド実装編⑦ ~大学・学部・研究室編集機能作成~
- その14: バックエンド実装編⑧ ~コメント投稿機能~
- その15: バックエンド実装編⑨ ~コメント編集・削除機能~
- その16: バックエンド実装編⑩ ~ブックマーク機能~
- その17: バックエンド実装編⑪ ~排他制御・トランザクション処理~
- その18: バックエンド実装編⑫ ~マイページ機能作成~
- その19: バックエンド実装編⑬ ~管理者アカウント機能作成~
- その20: バックエンド実装編⑭ ~通知機能作成~
- その21: バックエンド実装編⑮ ~ソーシャルログイン機能作成~
☆フロントエンド実装編
- その22: フロントエンド実装編① ~メインコンテンツ領域作成~
- その23: フロントエンド実装編② ~ヘッダー作成~
- その24: フロントエンド実装編③ ~サイドバー作成~
- その25: フロントエンド実装編④ ~ホームページ作成~
- その26: フロントエンド実装編⑤ ~大学検索結果画面作成~
- その27: フロントエンド実装編⑥ ~大学詳細・学部一覧画面作成~
- その28: フロントエンド実装編⑦ ~学部詳細・研究室一覧画面作成~
- その29: フロントエンド実装編⑧前編 ~研究室詳細・レビュー画面作成前編~
- その29.5: フロントエンド実装編⑧後編 ~研究室詳細・レビュー画面作成後編~
- その30: フロントエンド実装編⑨ ~マイページ作成~
- その31: フロントエンド実装編⑩ ~パンくずリスト作成~
- その32: フロントエンド実装編⑪ ~ログイン・新規登録モーダル作成~
- その33: フロントエンド実装編⑫ ~大学作成・編集モーダル作成~
- その34: フロントエンド実装編⑬ ~学部作成・編集モーダル作成~
- その35: フロントエンド実装編⑭ ~研究室作成・編集モーダル作成~
- その36: フロントエンド実装編⑮ ~レビュー作成・編集モーダル作成~
- その37: フロントエンド実装編⑯ ~コメント一覧表示モーダル作成~
- その38: フロントエンド実装編⑰ ~編集履歴ページ・削除依頼ページ・通知ドロップダウン作成~
- その39: フロントエンド実装編⑱ ~トースト作成~
- その40: フロントエンド実装編⑲ ~退会ページ作成~
- その41: フロントエンド実装編⑳ ~ファビコン作成~
- その42: フロントエンド実装編㉑ ~レスポンシブデザイン対応~
☆テスト・デバッグ編
- その43: テスト・デバッグ編① ~Unitテスト1[テストデータ準備・DB設定]~
- その44: テスト・デバッグ編② ~Unitテスト2[ポリシー]~
- その45: テスト・デバッグ編③ ~Unitテスト3[モデル]~
- その46: テスト・デバッグ編④ ~Featureテスト1[コントローラー①/レビュー/コメント/ブックマーク]~
- [その47: テスト・デバッグ編⑤ ~Featureテスト2コントローラー②/大学/学部/研究室~
- その48: テスト・デバッグ編⑥ ~Featureテスト3[コントローラー③/マイページ/削除依頼/通知/ホーム/管理者]~
- その49: テスト・デバッグ編⑦ ~Featureテスト4[ポリシー・モデル残り]~
軽く宣伝
YouTubeを始めました(というか始めてました)。
内容としては、Webエンジニアの生活や稼げるようになるまでの成長記録などを発信していく予定です。
現在、まったく再生されておらず、落ち込みそうなので、見てくださる方はぜひ高評価を教えもらえると励みになると思います。"(-""-)"
