Why use MailHog instead of Mail::fake()?
Laravel's Mail::fake() is great for unit testing — it lets you assert that a Mailable was dispatched. But it doesn't show you what the email looks like. You can't check HTML rendering, spam scores, or header correctness with Mail::fake().
MailHog captures the actual SMTP output — the same bytes that would reach a real mail server. You see the rendered HTML, check spam scoring, and inspect every header. This is essential for catching layout bugs, deliverability issues, and MIME encoding problems.
Quickstart: Laravel .env configuration
# .env (local development) MAIL_MAILER=smtp MAIL_HOST=smtp.mailhog.site MAIL_PORT=2525 MAIL_USERNAME=your-inbox-username MAIL_PASSWORD=your-inbox-password MAIL_ENCRYPTION=null MAIL_FROM_ADDRESS="noreply@yourapp.com" MAIL_FROM_NAME="Your App"
That's it. Every email Laravel sends — Mailables, Notifications, password resets — will be captured in your MailHog inbox.
Test it with artisan tinker
php artisan tinker
>>> Mail::raw('Hello from Laravel!', function ($m) {
... $m->to('test@example.com')
... ->subject('Test from Laravel');
... });
// Check MailHog dashboard — email appears instantly ✅Integration testing with PHPUnit
public function test_password_reset_sends_email()
{
$this->post('/forgot-password', [
'email' => 'user@test.com',
]);
// Query MailHog API
$response = Http::withHeaders([
'X-API-Key' => config('services.mailhog.api_key'),
])->get(config('services.mailhog.url') . '/api/v1/inbox/' .
config('services.mailhog.inbox_id') . '/messages', [
'to' => 'user@test.com',
]);
$emails = $response->json('data');
$this->assertNotEmpty($emails);
$this->assertStringContains('Reset', $emails[0]['subject']);
}Common pitfalls
- Setting
MAIL_ENCRYPTION=tlswith port 2525 — usenullinstead - Forgetting to clear the config cache after changing .env:
php artisan config:clear - Using
MAIL_MAILER=login CI instead of smtp — you miss real SMTP issues - Not testing queued emails — make sure your test runs the queue