0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Laravel 13 + WSLで始めるPlaywright【4. E2Eデータベース・fixture編】

0
Last updated at Posted at 2026-09-06

目次


はじめに

第3回では、Playwrightから接続するE2E専用Laravel serviceとしてlaravel.e2eを用意しました。

しかし、その時点ではまだE2E専用データベースを作っていません。

あえてDB接続を失敗させ、

E2E専用DBがない
↓
DB依存画面は失敗してよい
↓
通常開発DBへ誤接続するより安全

というfail-closed構成にしていました。

今回は、その続きとしてPlaywright専用のデータベース・MySQLユーザー・Laravel connection・reset CLI・Factory / Seederによるfixture基盤を構築します。

この回で特に重視したのは、E2Eテストのための破壊的なDBリセットが、通常開発DBやPHPUnit用DBへ絶対に向かないよう多層防御することです。


この記事で扱う範囲

この記事は、Issue #162で実装したE2Eデータベース・fixture基盤を振り返ります。

主な対象は次のとおりです。

E2E専用DB
E2E専用MySQLユーザー
最小権限
E2E_DB_* 環境変数
e2e_mysql connection
e2e:reset
Factory / Seeder
固定fixture
fail-closed自動テスト

Playwrightから実際にモーダルやページネーションを操作するブラウザテストは次回扱います。


環境

実装時点の主な環境です。

PHP             8.4.24
Laravel         13.26.1
Laravel Sail    1.58.0
MySQL           8.4
Node.js         24.19.0
npm             11.17.0
Playwright Test 1.62.1
TypeScript      7.0.2
WSL             Ubuntu 24.04

Laravel / MySQLはLaravel Sail、Playwright / Node.js / ChromiumはWSL側で管理しています。


3つのデータベースを分離する

今回のプロジェクトには、すでに通常開発用DBとPHPUnit用DBがありました。

そこへPlaywright用DBを追加します。

通常開発DB
→ laravel

PHPUnit用DB
→ testing

Playwright E2E用DB
→ e2e_testing

E2Eテストは、画面操作の結果としてINSERT / UPDATE / DELETEを実行します。

また、テスト開始前にはfixtureを毎回同じ状態へ戻すため、テーブルを削除してmigrationをやり直します。

そのため、

「設定を間違えても通常DBへ届かない」

ことを最優先にしました。


E2E専用MySQLユーザーを作る

E2E DBだけを操作するMySQLユーザーを用意します。

以下は一般化した例です。

CREATE DATABASE `e2e_testing`
    CHARACTER SET utf8mb4
    COLLATE utf8mb4_unicode_ci;

CREATE USER 'review_app_e2e'@'<allowed-host>'
    IDENTIFIED BY '<set-secure-password-outside-documentation>';

<allowed-host>は、Docker Composeから接続できる範囲で必要最小限にします。

注意

実パスワードは記事・Git・Issue・PR・ログへ記録しません。
また、'%'を一般的な推奨値として固定せず、実際の接続要件に応じてhost範囲を決めます。

今回のローカル環境では、E2E専用ユーザーがE2E DBだけへ接続できる状態を作りました。


最小権限にする

E2Eユーザーへは、migrationとfixture投入に必要な権限だけを付与します。

GRANT SELECT, INSERT, UPDATE, DELETE,
      CREATE, DROP, REFERENCES, INDEX, ALTER
ON `e2e_testing`.*
TO 'review_app_e2e'@'<allowed-host>';

MySQL公式では、これらの権限をdatabase単位で付与できます。

一方、次のような管理・global権限は付与しません。

CREATE USER
GRANT OPTION
FILE
PROCESS
ON *.*

ALL PRIVILEGESも使用しません。

MySQL公式も、アカウントには必要な権限だけを与えることを推奨しており、特にFILEや管理権限には注意するよう説明しています。


権限と拒否を確認する

付与後は、権限を確認します。

SHOW GRANTS FOR 'review_app_e2e'@'<allowed-host>';

確認するポイントは次のとおりです。

e2e_testing.*
→ 必要な権限だけある

WITH GRANT OPTION
→ ない

意図しないglobal権限
→ ない

さらにE2E用credentialで、

USE e2e_testing
→ 成功

USE laravel
→ Access denied

USE testing
→ Access denied

となることを確認しました。

これはLaravel側の設定ミスに対する二重防御になります。

仮にアプリケーションコード側で誤ったconnectionを選んでも、MySQLユーザー自身に通常DBへアクセスする権限がなければ書き込みできません。

SHOW GRANTSUSAGE ON *.*が表示される場合がありますが、MySQLにおけるUSAGEは「権限なし」を意味し、それ自体が追加のglobal権限を与えるものではありません。


E2E_DB_* を通常の DB_* と分ける

Laravelの通常DB設定と同じDB_*をE2Eでも使い回すと、設定不足時に通常DBへfallbackする余地が生まれます。

そこでE2E専用の名前空間を作りました。

.env.exampleには秘密情報ではない例だけを記載します。

E2E_DB_HOST=mysql
E2E_DB_PORT=3306
E2E_DB_DATABASE=e2e_testing
E2E_DB_USERNAME=review_app_e2e
E2E_DB_PASSWORD=e2e-password-not-configured

E2E_DB_PASSWORDは実credentialではなく、意図的に認証失敗するplaceholderです。

実値はGit管理しないローカル設定側で管理します。

今回の流れは次のとおりです。

E2E_DB_*
↓
compose.yaml
↓
laravel.e2e
↓
config/database.php
↓
e2e_mysql
↓
e2e_testing

ComposeでE2E用変数を必須にする

laravel.e2eへE2E専用変数を明示的に渡します。

DB_CONNECTION: e2e_mysql
DATABASE_URL: ''

E2E_DB_HOST: '${E2E_DB_HOST:?E2E_DB_HOST is required}'
E2E_DB_PORT: '${E2E_DB_PORT:?E2E_DB_PORT is required}'
E2E_DB_DATABASE: '${E2E_DB_DATABASE:?E2E_DB_DATABASE is required}'
E2E_DB_USERNAME: '${E2E_DB_USERNAME:?E2E_DB_USERNAME is required}'
E2E_DB_PASSWORD: '${E2E_DB_PASSWORD:?E2E_DB_PASSWORD is required}'

Docker Composeの${VAR:?error}は、変数が未設定または空の場合にエラーで停止するrequired interpolationです。

今回これを使った理由は、

E2E credentialがない
↓
通常DBのcredentialへfallback

という動きをさせないためです。

未設定なら早い段階で止めます。

運用上の注意

既存cloneのローカル設定に新しいE2E_DB_*がない場合、Composeの解析自体が失敗することがあります。
E2E基盤を追加した既存環境では、.env.exampleを参考に必要なE2E用変数を追加します。
実credentialはドキュメントへコピーしません。


E2E専用Laravel connectionを作る

config/database.phpへ通常のmysqlとは別にe2e_mysqlを追加しました。

概略は次のとおりです。

'e2e_mysql' => [
    'driver' => 'mysql',
    'url' => '',
    'host' => env('E2E_DB_HOST'),
    'port' => env('E2E_DB_PORT'),
    'database' => env('E2E_DB_DATABASE'),
    'username' => env('E2E_DB_USERNAME'),
    'password' => env('E2E_DB_PASSWORD'),
    'unix_socket' => env('E2E_DB_SOCKET', ''),
    'charset' => 'utf8mb4',
    'collation' => 'utf8mb4_unicode_ci',
    'prefix' => '',
    'prefix_indexes' => true,
    'strict' => true,
    'engine' => null,
],

重要なのは、

e2e_mysql
→ E2E_DB_*だけを見る

通常DB_*
→ fallbackとして使わない

ことです。

さらに、

'url' => '',

として、DATABASE_URLによる接続先上書きも使わない構成にしました。

Compose側でも、

DATABASE_URL: ''

としています。


通常の mysql connectionもE2E側で接続不能にする

e2e_mysqlを安全に作っても、E2Eコンテナ内でコードが明示的に、

DB::connection('mysql')

を呼べば通常connectionを選べます。

そこでlaravel.e2eでは、通常mysql側に使われる変数も意図的な無効値へ固定しました。

DB_HOST: e2e-database-not-configured
DB_USERNAME: e2e_not_configured
DB_PASSWORD: e2e_not_configured

つまり、

正規ルート
→ e2e_mysql
→ e2e_testing

誤ってmysqlを指定
→ 接続失敗

という構成です。

Laravel側とMySQL側の両方で通常DBへの経路を狭めています。


MySQLがHealthyになってからE2E Laravelを起動する

E2E LaravelはMySQLへ接続するため、起動順も調整しました。

depends_on:
    mysql:
        condition: service_healthy

Docker Composeでは、service_healthyを使うことで依存serviceのhealthcheck成功を待ってから起動できます。

今回の実測では、

mysql
→ Healthy

laravel.e2e
→ Started

という順序を確認しました。


e2e:reset を作る

E2Eテストでは、毎回同じDB状態から開始したいので専用Artisan commandを作りました。

php artisan e2e:reset

実装ファイルは、

app/Console/Commands/ResetE2eDatabase.php

です。

目的は単にDBを初期化することではありません。

e2e_testingだけを初期化する
通常DBを絶対に対象にしない
外部入力で対象connectionやDBを変えられない
途中失敗なら次へ進まない

ことが重要です。

そのためcommandのsignatureも、

protected $signature = 'e2e:reset';

だけにし、--databaseのような外部指定を受け付けません。

connectionとDB名はコード側で固定します。

private const CONNECTION = 'e2e_mysql';

private const DB_DATABASE = 'e2e_testing';

破壊処理の前に3レイヤーを確認する

db:wipeの前に、複数レイヤーで接続先を確認します。

1. Laravel実行環境
2. Laravel実効設定
3. MySQL実接続先

具体的には、

app()->environment('e2e')

でLaravelの実効環境を確認します。

次に、

config('database.connections.e2e_mysql.database')

でLaravelが現在認識しているDB名を確認します。

最後に、

SELECT DATABASE()

を実行し、MySQLが実際に接続しているDB名を確認します。

整理すると、

app()->environment('e2e')
→ 今のLaravelはE2E環境か

config(...)
→ Laravelの実効設定はe2e_testingか

SELECT DATABASE()
→ 本当にMySQLのe2e_testingへ接続しているか

です。

どれか1つでも不一致なら、破壊処理へ進まず異常終了します。

設定ファイルだけを信用せず、最後は実際の接続先まで照合する構成にしました。


migrate:fresh を使わなかった理由

最初は、

migrate:fresh
↓
db:seed

でresetする案でした。

しかしLaravel 13のFreshCommand実装を確認すると、内部でmigrateを呼び出したあと、その戻り値をFreshCommand自身の終了コードとして返していません。

Laravel 13の公式ソースでは概ね、

$this->call('migrate', [
    // ...
]);

// ...

return self::SUCCESS;

という流れです。

つまり、E2E専用reset command側で、

migrationが成功したことを終了コードで明示的に確認してからseedへ進む

という制御をしたい今回の用途には、そのままmigrate:freshを包むより各処理を分けた方が明確でした。

同じ理由で、破壊処理を1つの大きなcommandへ任せるのではなく、各段階をe2e:reset自身が監督します。


db:wipe → migrate → db:seed に分離する

最終的に次の順番にしました。

db:wipe
↓
終了コード確認
↓ 成功時のみ

migrate
↓
終了コード確認
↓ 成功時のみ

db:seed
↓
終了コード確認

各サブコマンドには固定connectionを渡します。

$this->call('db:wipe', [
    '--database' => 'e2e_mysql',
    '--force' => true,
]);

$this->call('migrate', [
    '--database' => 'e2e_mysql',
    '--force' => true,
]);

$this->call('db:seed', [
    '--class' => E2eSeeder::class,
    '--database' => 'e2e_mysql',
    '--force' => true,
]);

実際の実装では、それぞれの戻り値を個別に確認します。

wipe失敗
→ migrateしない

migrate失敗
→ seedしない

seed失敗
→ e2e:resetも失敗

全成功
→ exit code 0

Laravel公式ではdb:seed --class=...で特定Seederを実行でき、--forceによって確認プロンプトを回避できます。

今回はE2E専用command内で対象connectionとSeederを固定しているため、この仕組みを利用しています。


FactoryとSeederの役割を分ける

fixtureはNode.js / TypeScriptから直接SQLを実行して作りません。

Laravel側のFactory / Seederを使います。

役割は次のように分けました。

Factory
→ 1モデルをどう作るか

Seeder
→ E2Eテストで必要なデータセット全体をどう組み立てるか

今回の場合、

UserFactory
→ User 1件

ItemFactory
→ Item 1件

ReviewFactory
→ Review 1件

E2eSeeder
→ E2Eシナリオ用のユーザー・作品・レビュー一式

です。

Laravel公式でもSeederからEloquent model factoriesを利用できます。

この構成にすると、Laravel側のModel / Factoryで定義している生成ルールを再利用できます。


E2E fixtureを固定する

最終的なfixtureは次の構成にしました。

users      : 4
categories : 1
items      : 11
reviews    : 11

ユーザー状態も用途別に固定します。

E2E Reviewer
→ メール認証済み
→ レビュー11件の投稿者

E2E Unverified User
→ メール未認証

E2E No Review User
→ 認証済み
→ レビュー0件

E2E Verified User
→ 汎用の認証済みユーザー

作品名とレビュー本文もFakerへ任せず固定しました。

E2E Movie 01
...
E2E Movie 11
E2E Review 01
...
E2E Review 11

ratingも全件5に固定しています。

E2E fixtureでは、ランダムデータの幅広さより、

resetする
↓
毎回同じ画面になる
↓
同じlocator / assertionが使える

ことを優先しました。

rating自体の境界値やvalidationはPHPUnit側の責務としています。


なぜ11件なのか

一覧画面はpaginate(10)を使っています。

そのため、2ページ目を発生させる最小件数は、

10 + 1

の11件です。

11件
↓
1ページ目 10件
2ページ目 1件

これにより後続のPlaywrightテストで、

1ページ目
→ 最新側のデータを確認

次のページ
→ 11件目を確認

というページネーションテストができます。

大量のfixtureを作らず、境界値だけを作る構成です。


timestampも決定的にする

本文や作品名だけ固定しても、並び順が不定だとE2Eテストは不安定になります。

本人レビュー一覧ではlatest()、つまりcreated_at DESCで並びます。

複数レビューのcreated_atが同じ場合、同値行同士の順番までは保証されません。

そこで各レビューのtimestampを1分ずつずらしました。

概念的には、

E2E Review 11
→ 最新

E2E Review 10
→ 1分前

...

E2E Review 01
→ 最古

となるようにしています。

その結果、

/reviews/mine?page=1
→ E2E Review 11 ~ 02

/reviews/mine?page=2
→ E2E Review 01

というページ境界を毎回再現できます。

重要なのは、時刻の絶対値を固定することではありません。

reset時のnow()を基準にしながら、各レコード間の相対的な順序を決定的にしています。


resetの再現性を確認する

e2e:resetを複数回実行し、毎回同じ件数へ戻ることを確認しました。

users            = 4
items            = 11
reviews          = 11
verified_users   = 3
unverified_users = 1
reviewer_reviews = 11

主要な表示値も同じです。

first_item
→ E2E Movie 01

last_item
→ E2E Movie 11

first_review_body
→ E2E Review 01

first_review_rating
→ 5

timestamp調整後は、本人レビュー一覧と同じ並び条件で2ページ目が、

E2E Review 01

になることも実DB上で確認しました。


e2e:reset 自体もfail-closedでテストする

破壊的commandなので、「正常系だけ動く」では不十分です。

むしろ、

危険な条件で何もしない

ことを自動テストします。

tests/Feature/ResetE2eDatabaseTest.phpでは8ケースを用意しました。

1. E2E環境ではない
2. config上のDB名が違う
3. SELECT DATABASE()がnull
4. 実接続先DB名が違う
5. db:wipe失敗
6. migrate失敗
7. db:seed失敗
8. 正常系

テストでは実DBを破壊しません。

DB接続
→ Facade mock

db:wipe / migrate / db:seed
→ Command::call()を上書きしたtest double

としつつ、本物のResetE2eDatabase::handle()の制御フローは通します。

最終的にはSymfonyのCommandTesterを利用し、

CommandTester
↓
Laravel Command::run()
↓
本物のhandle()

というLaravel標準のConsole実行経路を通しました。

置き換えるのは、実際に破壊的サブコマンドを実行する境界だけです。

実装時の結果は、

8 tests passed
38 assertions

でした。


reset途中で失敗した場合

e2e:resetはfail-closedですが、rollback commandではありません。

たとえば、

db:wipe
→ 成功

migrate
→ 途中で失敗

した場合、e2e_testingは途中状態になり得ます。

この状態でPlaywrightテストを続けません。

原因を修正
↓
e2e:resetを最初から再実行
↓
正常終了を確認
↓
Playwrightを実行

という運用にします。

安全ガードの目的は、

通常DBを壊さない
誤った状態のままテストを続けない

ことであり、途中失敗したE2E DBを自動的に元へ戻すことではありません。


今回理解できたこと

今回のE2E DB基盤では、1つの設定だけで安全性を担保していません。

MySQLユーザー
→ e2e_testing.*だけに権限

E2E_DB_*
→ 通常DB設定と名前空間を分離

e2e_mysql
→ 通常DB_*へfallbackしない

通常mysql connection
→ E2E containerでは接続不能

e2e:reset
→ connection / DB名をコード側で固定

Laravel環境
→ e2eか確認

Laravel config
→ e2e_testingか確認

MySQL実接続
→ SELECT DATABASE()で確認

db:wipe / migrate / db:seed
→ 各終了コードを個別確認

fixture
→ 主要表示値と並び順を決定的にする

と、複数の層で守っています。

今回特に印象に残ったのは、

設定上そうなっている
≠
実際の接続先もそうなっている

という点です。

破壊的なDB操作の直前では、設定値だけでなく実接続先まで確認するようにしました。


まとめ

今回は、Playwright用E2Eデータベースと再現可能なfixture基盤を構築しました。

通常開発DB
→ laravel

PHPUnit DB
→ testing

Playwright E2E DB
→ e2e_testing

に分離し、

専用MySQLユーザー
最小権限
E2E_DB_*
e2e_mysql
通常connectionのfail-closed
e2e:reset
3レイヤーの安全ガード
db:wipe → migrate → db:seed
E2eSeeder
固定fixture
timestampによる順序固定
fail-closed自動テスト

まで整備しました。

これで、PlaywrightからDBを使う画面をテストしても、毎回同じE2Eデータから開始できるようになりました。

次回は、このfixtureを使って実際に、

モーダル
ARIA
初期フォーカス
Tab / Shift+Tab
Escape
フォーカス復帰
ページネーション

をChromiumで操作するブラウザテストを実装します。


公式一次情報

この記事の内容は、次の公式ドキュメント・公式ソースを確認しながら整理しています。

参照日: 2026-09-06
Laravel、MySQL、Dockerの仕様は更新されるため、実際に構築する際は最新版の公式一次情報も確認してください。


関連記事

0
1
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
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?