Mail::fake() 必须在被测代码执行前调用,否则 assertSent() 无法捕获邮件;断言时需传入完整命名空间的 ::class 常量,内容校验需通过闭包访问 $mail 实例属性。

Mail::fake() 之后为什么 assertSent() 总是失败?
核心原因:没在测试开始时调用 Mail::fake(),或调用位置不对。Laravel 的邮件门面(Mail)是单例,必须在被测代码执行前「替换」掉真实发送器,否则真实邮件驱动仍会尝试连接 SMTP,而 assertSent() 只能捕获 fake 驱动记录的邮件。
-
Mail::fake()必须放在测试方法开头,或在setUp()中统一调用 - 若被测逻辑里有
Mail::to(...)->send(...),但Mail::fake()写在 send 之后,断言必然失败 - 使用
Mail::fake(['log'])等指定驱动时,需确保与配置一致;默认用array驱动,已足够断言
assertSent() 和 assertNotSent() 的参数怎么写?
这两个方法接受类名字符串、闭包或 Mailable 实例。最常用的是传入 Mailable 类名 —— 但注意:必须是完整命名空间路径,且不能带 .php 后缀或引号外的斜杠。
- ✅ 正确:
Mail::assertSent(App\Mail\WelcomeEmail::class) - ❌ 错误:
Mail::assertSent('App\Mail\WelcomeEmail')(字符串形式不支持自动解析) - ✅ 带条件断言:
Mail::assertSent(App\Mail\WelcomeEmail::class, function ($mail) { return $mail->hasTo('user@example.com'); }) - ✅ 断言未发送:
Mail::assertNotSent(App\Mail\PasswordReset::class)
如何验证邮件内容(收件人、主题、变量)?
assertSent() 本身不校验内容,得通过闭包参数拿到实际构建的 Mailable 实例再检查。关键点在于:Mailable 对象在断言时已执行完 build(),所有 $this->with() 或构造注入的数据都可直接访问。
Mail::assertSent(App\Mail\InvoiceShipped::class, function ($mail) use ($invoice) {
return $mail->hasTo($invoice->user->email)
&& $mail->subject === 'Your Invoice #'.$invoice->number
&& $mail->invoice->id === $invoice->id;
});
-
$mail->hasTo()支持邮箱字符串、User 模型或集合,比手动查$mail->to数组更可靠 - 主题字段是
$mail->subject(不是$mail->getSubject()),因为 Laravel 在build()中已赋值 - 传递给 Mailable 构造函数的数据(如
new InvoiceShipped($invoice))会作为公共属性挂载,可直接读取
测试队列邮件时 fake 还管用吗?
管用,但必须确保队列任务真正执行了。Laravel 默认测试中队列是同步模式(sync),只要没显式改成 database 或 redis,Mail::fake() 就能捕获到延迟发送的邮件。
- 确认
QUEUE_CONNECTION=sync在 phpunit.xml 或测试环境配置中生效 - 如果用了
dispatch(new SendInvoiceEmail($invoice))->delay(now()->addSeconds(1)),需调用Mail::assertSent()前让队列运行:Queue::assertPushed(SendInvoiceEmail::class)不等于邮件已发,要等任务执行完毕 - 更稳妥做法:在测试中用
Bus::fake()或Queue::fake()配合Mail::fake(),然后手动调用app()->make(SendInvoiceEmail::class)->handle()绕过队列
Mail::fake() 是否真正在发送前生效,以及传给 assertSent() 的是不是 ::class 常量。









