Laravel Breeze標準のメール認証を有効化する中で、「会員登録したユーザーにメール認証通知が送信されること」をFeature Testで確認しました。
この記事では、完成したテストコードだけではなく、
Notification::fake()、User::where()、assertNull()、assertSentTo()を1行ずつ追加しながら、途中で発生したエラーも含めて整理します。
目次
- 環境
- 今回テストしたいこと
- まず完成形
- 1.
Notificationクラスとは何か - 2.
Notification::fake()で本物の通知送信を止める - 3. 会員登録のPOSTリクエストを送る
- 4.
$responseへ代入しなかった理由 - 5. 登録されたUserをDBから取得する
- 6. ここでエラーが発生した
- 7. 登録直後が未認証であることを確認する
- 8.
VerifyEmailクラスとは何か - 9.
Notification::assertSentTo()で通知を確認する ::classとは- 10. PHPDocでテストのWhyを残す
- テストを1件だけ実行する
- 関連テストもまとめて実行する
Notification::fake()からassertSentTo()までの関係- 今回覚えたテスト用メソッド
- 今回のテストをArrange / Act / Assertで整理する
- 今回のまとめ
- 関連記事
- 参考資料
環境
- PHP 8.4
- Laravel 13
- Laravel Breeze
- PHPUnit
- Laravel Sail
- MySQL
今回テストしたいこと
Laravel Breeze標準のメール認証を有効にすると、会員登録後は次の流れになります。
会員登録
↓
User作成
↓
Registeredイベント発火
↓
VerifyEmail通知
↓
認証メール送信
今回のFeature Testでは、特に次の2点を確認します。
1. 登録直後のユーザーはまだメール未認証である
2. そのユーザーへLaravel標準のVerifyEmail通知が送られる
つまり、
email_verified_at = null
+
VerifyEmail通知が送信される
という状態を保証するテストです。
まず完成形
今回完成したテストは次のとおりです。
/**
* 会員登録時にLaravel標準のメール認証通知が送信されることを保証する。
*/
public function test_new_users_receive_email_verification_notification(): void
{
Notification::fake();
$this->post('/register', [
'name' => 'Test User',
'email' => 'test@example.com',
'password' => 'password',
'password_confirmation' => 'password',
]);
$user = User::where('email', 'test@example.com')->firstOrFail();
$this->assertNull($user->email_verified_at);
Notification::assertSentTo(
$user,
VerifyEmail::class,
);
}
ここから、このコードをどのように1行ずつ組み立てたかを見ていきます。
1. Notificationクラスとは何か
まず、RegistrationTest.phpへ次を追加しました。
use Illuminate\Support\Facades\Notification;
ここで使っているNotificationは、自分で作った通知クラスではありません。
完全修飾クラス名は、
Illuminate\Support\Facades\Notification
です。
これはLaravelが用意しているNotification Facadeです。
LaravelのNotification機能は、メールだけに限定された仕組みではありません。Laravel公式ドキュメントでは、Notificationはメール、データベース、ブロードキャスト、SMS、Slackなど複数の配信チャネルへ通知を送る仕組みとして説明されています。
通常のアプリケーションコードでは、
Notification::send($users, new SomeNotification());
のようにFacade経由で通知を送ることもできます。
一方、今回のFeature Testでは「通知を送る」ためではなく、通知処理をテスト用に差し替える入口としてNotification Facadeを使います。
今回使うのは、
Notification::fake();
と、
Notification::assertSentTo();
です。
Facadeとは
LaravelのFacadeは、サービスコンテナで管理されている機能へ静的メソッド風の書き方でアクセスする仕組みです。
そのため、
Notification::fake();
という見た目でも、単なる自作staticメソッドを呼んでいるわけではありません。
LaravelのNotificationサービスへアクセスし、テスト時にはその実体をFakeへ差し替えるための窓口として使っています。
今回の理解としては、
Notification
↓
Laravelの通知機能へアクセスするFacade
↓
テストではfake()で本物の通知処理をFakeへ差し替えられる
と考えると分かりやすいです。
参考:
2. Notification::fake()で本物の通知送信を止める
テストメソッドの最初に次を追加しました。
Notification::fake();
通常なら、
会員登録
↓
VerifyEmail通知
↓
メール送信
と進みます。
しかしFeature Testで本当にメールを送る必要はありません。
Notification::fake()を使うと、
通知を実際には送信しない
↓
送ろうとした通知をテスト側で記録する
という状態になります。
イメージとしては、
通常
Notification
↓
メール送信
テスト
Notification
↓
fakeが捕まえて記録
です。
後から、
Notification::assertSentTo(...)
を使って「誰へ何の通知を送ろうとしたのか」を確認できます。
3. 会員登録のPOSTリクエストを送る
次に、通常の会員登録と同じデータをPOSTします。
$this->post('/register', [
'name' => 'Test User',
'email' => 'test@example.com',
'password' => 'password',
'password_confirmation' => 'password',
]);
今回は「メール認証通知」がテスト対象です。
そのため、会員登録自体は正常に成功する入力値を使います。
もしここで入力値を不正にすると、
会員登録に失敗した
↓
そもそもUserが作成されない
↓
通知も発生しない
となり、メール認証通知だけを確認するテストではなくなってしまいます。
テストでは、確認したい原因以外は正常な状態にしておくことが重要です。
4. $responseへ代入しなかった理由
最初は次のように書いていました。
$response = $this->post('/register', [
// ...
]);
しかし、このテストでは登録後のHTTPレスポンスを検証していません。
その後に、
$response->assertRedirect(...)
などを使わないので、$responseは不要です。
そのため最終的に、
$this->post('/register', [
// ...
]);
としました。
このPOSTは、
会員登録処理を実行するためだけ
という意図がコード上でも分かりやすくなります。
5. 登録されたUserをDBから取得する
次に、会員登録で作成されたユーザーを取得します。
$user = User::where('email', 'test@example.com')->firstOrFail();
分解すると、
User::where(...)
↓
emailがtest@example.comのUserを検索
firstOrFail()
↓
最初の1件を取得
↓
存在しなければ失敗
となります。
今回、通知先のユーザーをNotification::assertSentTo()へ渡す必要があるため、DBから実際に登録されたUserモデルを取得しています。
6. ここでエラーが発生した
最初にテストを実行したところ、次のエラーが出ました。
Class "Tests\Feature\Auth\User" not found
原因は、RegistrationTest.phpでUserモデルをimportしていなかったことでした。
テストクラスのnamespaceは、
namespace Tests\Feature\Auth;
です。
この状態で、
User::where(...)
と書くと、PHPはUserを現在のnamespaceから探そうとします。
つまり、
Tests\Feature\Auth\User
を探してしまいます。
しかし、本当に使いたいのは、
App\Models\User
です。
そこで次を追加しました。
use App\Models\User;
これによって、
User::where(...)
のUserがApp\Models\Userを指すようになりました。
この修正後、テストは先へ進めるようになりました。
7. 登録直後が未認証であることを確認する
取得したユーザーに対して、次を追加しました。
$this->assertNull($user->email_verified_at);
assertNull()は、
値が
nullであること
を確認するPHPUnitのAssertionです。
Laravelのメール認証では、
email_verified_at = null
なら未認証、
email_verified_at = 日時
なら認証済みです。
そのため、
$this->assertNull($user->email_verified_at);
によって、
会員登録直後のユーザーはまだメール認証済みではない
ことを確認できます。
8. VerifyEmailクラスとは何か
次に、何の通知が送信されたのかを確認するため、Laravel標準のメール認証通知クラスをimportしました。
use Illuminate\Auth\Notifications\VerifyEmail;
完全修飾クラス名は、
Illuminate\Auth\Notifications\VerifyEmail
です。
このVerifyEmailは、先ほどのNotification Facadeとは役割がまったく違います。
Notification
↓
通知機能を操作するためのFacade
VerifyEmail
↓
実際に送る「メール認証通知そのもの」を表すNotificationクラス
という関係です。
Laravelでは、1つの通知内容を1つのNotificationクラスとして表現します。
例えば自作アプリなら、
InvoicePaid
PasswordChanged
CommentPosted
のようなNotificationクラスを作ることができます。
今回のVerifyEmailはそれをLaravel自身が標準で用意しているものです。
VerifyEmailは何をするクラスなのか
Laravelのメール認証では、ユーザーに「メールアドレスの所有者であることを確認するリンク」を送ります。
VerifyEmailは、そのメール認証通知を表すクラスです。
今回のUserモデルはMustVerifyEmailを実装しました。
Laravel 13のフレームワーク側では、メール認証通知を送る処理で、
$this->notify(new VerifyEmail);
という形で、このVerifyEmail Notificationがユーザーへ送られます。
つまり流れは、
UserがMustVerifyEmailを実装
↓
会員登録
↓
Registeredイベント
↓
メール認証通知を送る処理
↓
new VerifyEmail
↓
Userへ通知
となります。
VerifyEmailが生成するメールには、メール認証用のリンクが含まれます。
今回のテストではメール本文そのものを確認するのではなく、
登録ユーザーへLaravel標準の
VerifyEmailNotificationが送信対象になったか
を確認しています。
NotificationとVerifyEmailを混同しない
この2つは名前が似ているため、最初は混乱しやすいところです。
| 名前 | 種類 | 役割 |
|---|---|---|
Illuminate\Support\Facades\Notification |
Facade | Laravelの通知機能を操作する入口 |
Illuminate\Auth\Notifications\VerifyEmail |
Notificationクラス | メール認証という具体的な通知内容を表す |
今回のコード、
Notification::assertSentTo(
$user,
VerifyEmail::class,
);
を日本語にすると、
Laravelの通知システムに記録された内容を確認し、
$userへVerifyEmailという種類の通知が送られたことを検証する
という意味になります。
参考:
- Laravel 13.x - Notifications
- Laravel 13.x - Email Verification
- Laravel Framework 13.x - MustVerifyEmail
9. Notification::assertSentTo()で通知を確認する
最後に、
Notification::assertSentTo(
$user,
VerifyEmail::class,
);
を追加しました。
assertSentTo()は、
指定したユーザーへ、指定したNotificationが送られたこと
を確認するAssertionです。
今回の場合、
$user
が通知先、
VerifyEmail::class
が通知の種類です。
つまり、
登録した$userへ
↓
VerifyEmail通知が送られた
ことを確認しています。
::classとは
今回初めて、
VerifyEmail::class
という書き方も使いました。
PHPの::classは、クラス名を完全修飾クラス名として取得するための構文です。
今回なら、概念的には、
VerifyEmail::class
↓
Illuminate\Auth\Notifications\VerifyEmail
を表します。
文字列で完全修飾クラス名を直接書く代わりに、PHPのクラス名解決を使って完全修飾クラス名の文字列を取得できます。
10. PHPDocでテストのWhyを残す
完成したテストには、次のPHPDocを付けました。
/**
* 会員登録時にLaravel標準のメール認証通知が送信されることを保証する。
*/
テストメソッド名でも、
new users receive email verification notification
という「何を確認するか」は分かります。
PHPDocでは、
会員登録フローの中でLaravel標準のメール認証通知が欠けないことを保証する
というテストの意図を日本語で残しています。
テストを1件だけ実行する
実装途中では、追加したテストだけを実行しました。
sail artisan test tests/Feature/Auth/RegistrationTest.php \
--filter=new_users_receive_email_verification_notification
最終的な結果は、
PASS Tests\Feature\Auth\RegistrationTest
✓ new users receive email verification notification
Tests: 1 passed (2 assertions)
となりました。
2つのAssertionは主に、
assertNull()
↓
登録直後が未認証
assertSentTo()
↓
VerifyEmail通知が送られた
に対応しています。
関連テストもまとめて実行する
このテスト単体がPASSした後、関連するFeature Testもまとめて実行しました。
sail artisan test \
tests/Feature/Auth/RegistrationTest.php \
tests/Feature/ReviewTest.php \
tests/Feature/ReviewCommentStoreTest.php
結果は、
Tests: 37 passed (347 assertions)
でした。
新しいメール認証テストだけではなく、
- 既存の会員登録
- レビュー投稿
- レビュー返信
にも回帰がないことを確認できました。
Notification::fake()からassertSentTo()までの関係
今回の処理をクラスの役割まで含めて並べると、次のようになります。
Illuminate\Support\Facades\Notification
│
├─ fake()
│ └─ 本物の通知送信をFakeへ差し替える
│
└─ assertSentTo()
└─ Fakeに記録された通知を検証する
Illuminate\Auth\Notifications\VerifyEmail
│
└─ 「メール認証通知」という具体的な通知の種類
テスト全体では、
Notification::fake()
↓
会員登録
↓
LaravelがVerifyEmail通知を送ろうとする
↓
本物のメール送信は行われずFakeが記録
↓
Notification::assertSentTo(...)
↓
$userへVerifyEmailが送られたことを確認
となります。
ここを理解すると、
Notification::assertSentTo(
$user,
VerifyEmail::class,
);
が単なる暗記ではなく、
誰に?
$user
何を?
VerifyEmail
本当に送った?
本番送信はせず、Fakeに記録された内容を検証
という意味だと分かります。
今回覚えたテスト用メソッド
| メソッド | 役割 |
|---|---|
Notification::fake() |
実際の通知送信を止め、テスト用に記録する |
$this->post() |
POSTリクエストをLaravel内部で再現する |
User::where() |
条件を指定してUserを検索する |
firstOrFail() |
最初の1件を取得し、なければ失敗させる |
assertNull() |
値がnullであることを確認する |
Notification::assertSentTo() |
指定ユーザーへ指定Notificationが送られたことを確認する |
::class |
クラスの完全修飾名を文字列として取得する |
今回のテストをArrange / Act / Assertで整理する
今回のテストも、Feature Testの基本であるArrange / Act / Assertに整理できます。
Arrange
Notification::fake()
Act
会員登録POST
Assert
登録Userを取得
↓
email_verified_atがnull
↓
VerifyEmail通知が送られた
厳密にはUser取得はAssertの準備でもありますが、
準備
操作
確認
という3段階で考えるとテストの流れが分かりやすくなります。
今回のまとめ
今回のFeature Testでは、
Notification::fake()
↓
会員登録POST
↓
DBから登録User取得
↓
assertNull(email_verified_at)
↓
assertSentTo(VerifyEmail)
という流れで、Laravel Breeze標準のメール認証通知を確認しました。
特に印象に残ったのは、
本物のメールを送らなくても
Notification::fake()で通知処理をテストできる
という点です。
また、
Class "Tests\Feature\Auth\User" not found
というエラーから、
namespaceがあるPHPコードでは、使用するモデルを正しく
useでimportする必要がある
ことも実際のエラーを通して確認できました。
完成コードだけを見るより、1行ずつ意味を確認し、失敗した原因を直しながら進めることで、Laravel Feature Testの仕組みを理解しやすくなりました。
関連記事
- Laravel Feature Testでメール認証を検証する:署名付きURL・期限切れ・別ユーザー・認証済み分岐まで
- Laravel Feature Testを1行ずつ理解する:メール未認証ユーザーのレビュー投稿・返信をテストする
- Laravel Feature TestにPHPDocで「Why」を残す:テストの意図を読みやすくする
- PHPUnit・Laravel Featureテストでよく使う英単語集