実務1年目駆け出しエンジニアがLaravel&ReactでWebアプリケーション開発に挑戦してみた!(その53)
0. 初めに
こんにちは!
1年目の見習いエンジニアが、Webアプリケーションをゼロから開発する様子をお届けしているシリーズ、テスト編です!
ついに今日で、E2Eテスト、ひいてはテストの最後です!
よろしくお願いいたします。
1. ブランチ運用
developブランチを最新化させて、新規にtest/e2e/adminというブランチを切って作業をしましょう。
前回使用したtest/e2e/general-userブランチは削除しておきましょう。
2. 管理者
import { test, expect } from '@playwright/test';
import { AUTH } from './helpers/auth-paths.js';
// 管理者機能のテスト
// シードデータ前提:
// admin@example.com (is_admin=true)
// user@example.com (一般ユーザー)
// University id=1: テストA国立大学
// Faculty id=5: 工学部 (university_id=1)
// Lab id=1: 機械工学科 テストA研究室 (faculty_id=5)
test.use({ storageState: AUTH.admin });
// ─────────────────────────────────────────────
// 削除依頼一覧
// ─────────────────────────────────────────────
test.describe('管理者: 削除依頼一覧', () => {
test('削除依頼一覧ページにアクセスできる', async ({ page }) => {
await page.goto('/admin/deletion-requests');
await expect(page.getByText('削除依頼一覧')).toBeVisible();
await expect(page.getByRole('button', { name: 'マイページに戻る' })).toBeVisible();
});
test('削除依頼がない場合は空状態メッセージが表示される', async ({ page }) => {
await page.goto('/admin/deletion-requests');
await expect(page.getByText('削除依頼はありません。')).toBeVisible();
});
test('ユーザーが削除依頼を提出すると一覧に表示される', async ({ page, browser }) => {
// user コンテキストで削除依頼を提出
const userContext = await browser.newContext({ storageState: AUTH.user });
const userPage = await userContext.newPage();
await userPage.goto('/deletion-requests/create/university/1');
await userPage.getByPlaceholder('削除を希望する理由をご記入ください。').fill('E2Eテスト: 管理者向け削除依頼');
await userPage.getByRole('button', { name: '送信' }).click();
await expect(userPage.getByText('削除依頼をお送りいただき、ありがとうございました。')).toBeVisible({ timeout: 10000 });
await userContext.close();
// 管理者で削除依頼一覧を確認
await page.goto('/admin/deletion-requests');
await expect(page.getByText('テストA国立大学').first()).toBeVisible();
await expect(page.getByText('(大学)への削除依頼')).toBeVisible();
await expect(page.getByText('E2Eテスト: 管理者向け削除依頼')).toBeVisible();
await expect(page.getByText(/依頼者:/)).toBeVisible();
});
test('削除依頼の対象名リンクから対象ページに遷移できる', async ({ page, browser }) => {
// user コンテキストで削除依頼を提出(前提データ)
const userContext = await browser.newContext({ storageState: AUTH.user });
const userPage = await userContext.newPage();
await userPage.goto('/deletion-requests/create/university/1');
await userPage.getByPlaceholder('削除を希望する理由をご記入ください。').fill('E2Eテスト: 遷移確認');
await userPage.getByRole('button', { name: '送信' }).click();
await expect(userPage.getByText('削除依頼をお送りいただき、ありがとうございました。')).toBeVisible({ timeout: 10000 });
await userContext.close();
// 管理者でリストの対象名をクリック
await page.goto('/admin/deletion-requests');
await page.getByText('テストA国立大学').first().click();
await expect(page).toHaveURL(/\/universities\/1\/faculties/);
});
test('「マイページに戻る」ボタンでマイページに遷移できる', async ({ page }) => {
await page.goto('/admin/deletion-requests');
await page.getByRole('button', { name: 'マイページに戻る' }).click();
await expect(page).toHaveURL(/\/mypage/);
});
});
// ─────────────────────────────────────────────
// アクセス権限
// ─────────────────────────────────────────────
test.describe('管理者: アクセス権限', () => {
test('一般ユーザーは削除依頼一覧にアクセスできない', async ({ browser }) => {
const userContext = await browser.newContext({ storageState: AUTH.user });
const userPage = await userContext.newPage();
const response = await userPage.goto('/admin/deletion-requests');
// abort(403) によりアクセスが拒否されること
expect(response.status()).toBe(403);
await userContext.close();
});
test('未ログインユーザーは削除依頼一覧にアクセスするとリダイレクトされる', async ({ browser }) => {
const guestContext = await browser.newContext({ storageState: { cookies: [], origins: [] } });
const guestPage = await guestContext.newPage();
await guestPage.goto('/admin/deletion-requests');
await expect(guestPage).not.toHaveURL(/\/admin\/deletion-requests/);
await guestContext.close();
});
});
実行コマンド
$ npx playwright test e2e/admin.spec.js
コミットしておきましょう!
3. 通知
import { test, expect } from '@playwright/test';
import { AUTH } from './helpers/auth-paths.js';
import { openMenuAndClick } from './helpers/kebab-menu.js';
// 通知機能のテスト
// シードデータ前提:
// admin@example.com (is_admin=true, University id=1「テストA国立大学」の作成者)
// user@example.com (一般ユーザー)
//
// 通知の種類:
// DeletionRequestNotification : 削除依頼時に管理者に通知
// ModelChangedNotification : コンテンツ編集時に作成者に通知
// DeletionCompletedNotification: 削除完了時に依頼者に通知(削除UIが未実装のためテスト対象外)
// ─────────────────────────────────────────────
// 通知: 一覧ページ (ユーザー)
// ─────────────────────────────────────────────
test.describe('通知: 一覧ページ', () => {
test.use({ storageState: AUTH.user });
test('/notifications にアクセスできる', async ({ page }) => {
await page.goto('/notifications');
await expect(page.getByText('通知一覧')).toBeVisible();
});
test('通知がない場合は「通知はありません。」が表示される', async ({ page }) => {
await page.goto('/notifications');
await expect(page.getByText('通知はありません。')).toBeVisible();
});
test('「< マイページに戻る」でマイページに遷移できる', async ({ page }) => {
await page.goto('/notifications');
await page.getByRole('button', { name: '< マイページに戻る' }).click();
await expect(page).toHaveURL(/\/mypage/);
});
});
// ─────────────────────────────────────────────
// 通知: 削除依頼通知 (管理者)
// ─────────────────────────────────────────────
test.describe('通知: 削除依頼通知', () => {
test.use({ storageState: AUTH.admin });
test('ユーザーが削除依頼を提出すると管理者に通知が届く', async ({ page, browser }) => {
// user コンテキストで削除依頼を提出
const userContext = await browser.newContext({ storageState: AUTH.user });
const userPage = await userContext.newPage();
await userPage.goto('/deletion-requests/create/university/1');
await userPage.getByPlaceholder('削除を希望する理由をご記入ください。').fill('E2Eテスト: 通知確認');
await userPage.getByRole('button', { name: '送信' }).click();
await expect(userPage.getByText('削除依頼をお送りいただき、ありがとうございました。')).toBeVisible({ timeout: 10000 });
await userContext.close();
// 管理者の通知一覧を確認
await page.goto('/notifications');
await expect(page.getByText('「テストA国立大学」に削除依頼が届きました。').first()).toBeVisible({ timeout: 10000 });
});
test('削除依頼通知をクリックすると削除依頼一覧ページに遷移できる', async ({ page, browser }) => {
// user コンテキストで削除依頼を提出
const userContext = await browser.newContext({ storageState: AUTH.user });
const userPage = await userContext.newPage();
await userPage.goto('/deletion-requests/create/university/1');
await userPage.getByPlaceholder('削除を希望する理由をご記入ください。').fill('E2Eテスト: 通知クリック遷移確認');
await userPage.getByRole('button', { name: '送信' }).click();
await expect(userPage.getByText('削除依頼をお送りいただき、ありがとうございました。')).toBeVisible({ timeout: 10000 });
await userContext.close();
// 管理者の通知一覧から通知をクリックして遷移を確認(最初の通知をクリック)
await page.goto('/notifications');
await page.getByText('「テストA国立大学」に削除依頼が届きました。').first().click();
await expect(page).toHaveURL(/\/admin\/deletion-requests/);
});
});
// ─────────────────────────────────────────────
// 通知: モデル変更通知 (管理者)
// ─────────────────────────────────────────────
test.describe('通知: モデル変更通知', () => {
test.use({ storageState: AUTH.admin });
test('ユーザーがコンテンツを編集すると作成者(管理者)に通知が届く', async ({ page, browser }) => {
// user コンテキストで大学(admin 作成)を編集 → admin に通知が届く
const userContext = await browser.newContext({ storageState: AUTH.user });
const userPage = await userContext.newPage();
await userPage.goto('/universities/1/faculties');
await openMenuAndClick(userPage, '編集する');
await expect(userPage.getByText('大学を編集する')).toBeVisible({ timeout: 10000 });
// 大学名はそのまま、編集理由だけ入力して送信(大学名を変えないことで他テストへの影響を避ける)
await userPage.getByPlaceholder('編集理由を入力してください').fill('E2Eテスト: 通知確認のための編集');
await userPage.getByRole('button', { name: '編集する' }).click();
await expect(userPage.getByRole('link', { name: 'テストA国立大学' })).toBeVisible({ timeout: 10000 });
await userContext.close();
// 管理者の通知一覧に変更通知が表示されることを確認
await page.goto('/notifications');
await expect(
page.getByText('あなたが作成した「テストA国立大学(大学)」が編集されました。').first()
).toBeVisible({ timeout: 10000 });
});
});
// ─────────────────────────────────────────────
// 通知: ドロップダウン (管理者)
// ─────────────────────────────────────────────
test.describe('通知: ドロップダウン', () => {
test.use({ storageState: AUTH.admin });
test('未読通知がある場合、ベルアイコンに未読バッジが表示される', async ({ page, browser }) => {
// user コンテキストで削除依頼を提出して通知を生成
const userContext = await browser.newContext({ storageState: AUTH.user });
const userPage = await userContext.newPage();
await userPage.goto('/deletion-requests/create/university/1');
await userPage.getByPlaceholder('削除を希望する理由をご記入ください。').fill('E2Eテスト: バッジ確認');
await userPage.getByRole('button', { name: '送信' }).click();
await expect(userPage.getByText('削除依頼をお送りいただき、ありがとうございました。')).toBeVisible({ timeout: 10000 });
await userContext.close();
// 管理者のマイページでベルアイコンの未読バッジを確認
await page.goto('/mypage');
const bellButton = page.getByRole('button', { name: '通知' });
await expect(bellButton.locator('span.bg-red-500')).toBeVisible({ timeout: 10000 });
});
test('ベルアイコンをクリックするとドロップダウンに通知が表示される', async ({ page, browser }) => {
// user コンテキストで削除依頼を提出して通知を生成
const userContext = await browser.newContext({ storageState: AUTH.user });
const userPage = await userContext.newPage();
await userPage.goto('/deletion-requests/create/university/1');
await userPage.getByPlaceholder('削除を希望する理由をご記入ください。').fill('E2Eテスト: ドロップダウン表示確認');
await userPage.getByRole('button', { name: '送信' }).click();
await expect(userPage.getByText('削除依頼をお送りいただき、ありがとうございました。')).toBeVisible({ timeout: 10000 });
await userContext.close();
// 管理者がベルアイコンをクリックしてドロップダウンを開く
await page.goto('/mypage');
await page.getByRole('button', { name: '通知' }).click();
// ドロップダウンに通知メッセージが表示される
await expect(page.getByText('「テストA国立大学」に削除依頼が届きました。').first()).toBeVisible({ timeout: 10000 });
// フッターの「すべての通知を見る」リンクが表示される
await expect(page.getByRole('link', { name: 'すべての通知を見る' })).toBeVisible();
});
});
// ─────────────────────────────────────────────
// 通知: アクセス権限
// ─────────────────────────────────────────────
test.describe('通知: アクセス権限', () => {
test('未ログインは /notifications にアクセスするとリダイレクトされる', async ({ browser }) => {
const guestContext = await browser.newContext({ storageState: { cookies: [], origins: [] } });
const guestPage = await guestContext.newPage();
await guestPage.goto('/notifications');
await expect(guestPage).not.toHaveURL(/\/notifications/);
await guestContext.close();
});
});
実行コマンド
$ npx playwright test e2e/notification.spec.js
コミット・プッシュ、PR作成・マージ、ブランチ削除を行いましょう!
4. まとめ
これにて、テスト・デバッグ編完了です!!
5. 感謝とこれからについて
2025年の5月から初めて、もう2026年の9月になり、長いもので1年と4か月ほどもこのシリーズを続けてきました。
もともとは、「早く実務経験を積みたい」という気持ちがある一方で、入社後は研修期間が長くそのギャップへの抵抗の気持ちから始めました。
しかし、今となっては間もなく実務開始から1年が経過しようとしています。
このシリーズでたくさんことを学ぶことができた一方で、ポートフォリオとしては機能数が多く実際の実装も2026年の5月まで1年近く続いてしまいました。
今振り返れば、その間もっといろんな別のことに挑戦できたかもしれなかったと感じています。
ですが、普段の本業の業務では経験できないような「環境構築・設計・バックエンド・フロントエンド・テスト・デプロイ」をすべて自分一人で行うことで幅広い知識を身に着けることができましたし、本業に少なからず良い影響を与えたと感じています。
今後は、このシリーズを通じて学んだことを発信していければと思います。
また、デプロイ編についてですが、AWSを使って5月にとっくにデプロイ済みですが、シリーズとしての投稿はしない予定です。
コードを書くことが少なく、AWSのUIも頻繁に変わりますし、何より僕のモチベーションがもうないので...
ただし、AWSやクラウドに関連する知識(「サブネット」や「EC2」、「RDS」などの基本)を学ぶことはできました。
それについては、どこかで発信できればと思っています。
今後は、学んだことを生かして、副業などに挑戦したいと思います。
もし、ご縁があれば、声をかけてくださるとうれしいです!
最後に、ここまで読んでくださって本当にありがとうございました。
これまでの記事一覧
☆要件定義・設計編
☆環境構築編
☆バックエンド実装編
- その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[ポリシー・モデル残り]~
- その50: テスト・デバッグ編⑧)~E2Eテスト1[Playwrightセットアップ]~
- その51: テスト・デバッグ編⑨)~E2Eテスト2[ゲスト]~
- その52: テスト・デバッグ編⑨)⑩)~E2Eテスト3[一般ユーザー]~
軽く宣伝
YouTubeを始めました(というか始めてました)。
内容としては、Webエンジニアの生活や稼げるようになるまでの成長記録などを発信していく予定です。

