Application email QA
Test welcome email delivery in Laravel: assertions and cleanup
Assert Laravel welcome mail generation in automated tests, then manually confirm SMTP handoff with a receive-only inbox—without treating Mailby as a CI API.

For welcome email delivery in Laravel, assert application generation with Mail::fake() (or notification fakes) in PHPUnit/Pest, then separately verify real SMTP handoff to a mailbox you control. Use a receive-only temporary inbox for manual inbound checks. Do not assume a Mailby CI API is required for the unit layer—and when you use the live developer tools, treat them as the product’s documented API/console, not as a substitute for Laravel’s mail fakes.
This guide separates four layers so failures stay diagnosable.
Laravel context and boundaries
Welcome mail usually means: user registers → app queues/sends a WelcomeMail mailable or notification → SMTP/API provider accepts it → user inbox renders it.
Layers:
- Application generation — correct recipient, subject, view data
- SMTP / API handoff — credentials, queues, rate limits
- Inbound delivery — provider → recipient MX
- User experience — HTML/text rendering, CTA links
Automated Laravel tests shine at layer 1. Layers 2–4 need staging sends or manual receive checks.
Product truth: Mailby Quick Inbox is live at /inbox for receive-only capture. Developer/Test Inbox Cloud API and console are live at /developers and /account/developer for programmatic receive workflows when that fits your stack. Laravel’s first-line unit tests should still use framework fakes.
Minimal authorized test (layer 1)
use App\Mail\WelcomeMail;
use App\Models\User;
use Illuminate\Support\Facades\Mail;
public function test_welcome_mail_is_generated_for_new_user(): void
{
Mail::fake();
$user = User::factory()->create([
'email' => 'new-user@example.test',
]);
// Trigger the same path registration uses (event, action, or job)
$this->post('/register', [
'name' => 'Ada',
'email' => $user->email,
'password' => 'password',
'password_confirmation' => 'password',
]);
Mail::assertSent(WelcomeMail::class, function (WelcomeMail $mail) use ($user) {
return $mail->hasTo($user->email)
&& $mail->assertHasSubject('Welcome'); // adjust to your API
});
}
Adapt to your Laravel version’s assertion helpers (assertQueued if you queue mailables). The point is: no real network in the default test suite.
Negative cases
- Registration validation fails →
Mail::assertNothingSent() - Duplicate email → no second welcome
- User opts out of marketing but still receives transactional welcome if that is your policy—assert the distinction explicitly
Field evidence: working path (manual receive)
On 2026-09-24, against a local Laravel app with Mailpit/log driver swapped to a real SMTP staging account:
- Registered a user whose address was a Mailby Quick Inbox receive-only address.
- Confirmed the mailable fired in logs.
- Opened
/inboxsession, received the welcome HTML. - Checked that the CTA pointed at the staging app host (not production).
Working path: End-to-end receive confirmed without polluting a personal mailbox.
Failure / limitation
When QUEUE_CONNECTION=database and the queue worker was stopped, Laravel “sent” nothing to SMTP. Unit tests with Mail::fake() still passed. Lesson: green unit tests do not prove delivery. Always keep a smoke path that observes a real message.
Test case table
| Test case | Expected message | Observable evidence | Negative case |
|---|---|---|---|
| Happy registration | Welcome mailable to user | Mail::assertSent / inbox receive | Validation error → nothing sent |
| Queued welcome | Job pending then sent | assertQueued + worker processes | Worker down → no SMTP |
| Wrong env mailer | Staging host in links | HTML contains staging URL | Production URL leak |
| Localized welcome | Correct locale template | Subject/body language | Fallback locale unexpectedly |
| Soft-deleted user | No welcome | assertNothingSent | Mail still generated |
Worked example: cleanup
Temporary addresses and staging users accumulate.
- Delete staging users tagged
qa-welcome-*. - Flush Redis/database queues between suites.
- Rotate SMTP sandbox credentials in CI secrets—not in git.
- Close Quick Inbox sessions when finished; do not screenshot live addresses into public tickets.
For browser-driven identity isolation patterns, see isolating test identities in Selenium.
SMTP handoff checklist (layer 2)
MAIL_MAILER, host, port, encryption match provider docs (Laravel mail docs)- From address aligns with SPF/DKIM domains
- Queue retries have backoff; poison messages are visible
- Failures land in logs/Sentry, not silent
report()swallows
When not to use a temporary inbox
- Testing password reset for production users
- Capturing real customer PII
- Any flow that requires the test to send mail from the temporary address (Mailby will not)
Prefer durable shared QA mailboxes for long-lived staging seed accounts; use temporary receive-only for one-shot delivery proofs.
Short answers
What causes welcome email delivery bugs in Laravel?
Misconfigured mailers, stopped queues, wrong env, or asserting the wrong layer.
What should I do first?
Mail::fake() assertions on the registration path, then one staging SMTP smoke receive.
When is a permanent address safer?
Shared staging identities that must receive mail for weeks.
What evidence changes the recommendation?
You need automated inbound assertions via API—then evaluate /developers against your CI design rather than scraping a browser inbox.
Sources, test date, limitations
- Narrative date: 2026-09-24.
- External: Laravel Mail documentation.
- Examples are illustrative; adjust to your Laravel major version.
- Mailby does not send/forward; receive-only observation only.
Expanding the Laravel test pyramid for mail
Feature tests vs unit tests
Unit-level: assert a listener or action builds WelcomeMail with the right user.
Feature-level: post to /register and assert sent/queued.
HTTP-level smoke: optional staging send to a receive-only inbox.
Keep network I/O out of the default PR gate. Run smoke on a schedule or pre-release job.
Notifications vs Mailables
Laravel apps often use Notification channels. Fake with Notification::fake() and assert MailMessage contents. Do not mix Mail::fake() expectations if the code path only notifies.
Queues, Horizon, and failed jobs
Welcome mail delayed by a queue looks like “delivery broken” in Selenium even when generation tests pass. Monitor failed_jobs. Add a test that WelcomeMail is queued when that is the design.
Localization and themes
If you brand welcome mail per tenant, assert view data includes tenant.name and that the HTML does not leak another tenant’s logo path—common multi-tenant bug.
Link correctness
Parse the CTA URL in tests:
Mail::assertSent(WelcomeMail::class, function ($mail) {
$html = $mail->render();
return str_contains($html, config('app.url'));
});
Prevent production URLs in staging and vice versa.
Manual receive protocol
- Set staging SMTP to a sandbox provider or relay you control.
- Register with a Mailby address from
/inboxor an API identity from/developers. - Confirm subject, From alignment, and CTA host.
- Record the message-id in the QA ticket; then let the temporary inbox expire.
Cleanup automation ideas
- Nightly job deletes users with
email like 'qa-%' - CI uses unique emails per run (UUID)
- Never commit captured message bodies containing real customer data
How this differs from the test-email-workflows hub
Hub pages survey strategies. This article is Laravel-specific: fakes, queues, and a receive-only confirmation path with explicit product boundaries for Mailby.
Example Pest test sketch
it('sends welcome mail after registration', function () {
Mail::fake();
$response = $this->post('/register', validRegistrationPayload());
$response->assertRedirect();
Mail::assertSent(WelcomeMail::class);
});
it('does not send welcome mail when registration fails', function () {
Mail::fake();
$this->post('/register', invalidRegistrationPayload());
Mail::assertNothingSent();
});
Keep payloads in factories. Assert nothing sent on failure—this catches accidental mail in validation error paths.
Staging smoke checklist (copy/paste)
-
APP_URLis staging - Mail From domain authorized
- Queue worker running
- Register QA user with unique email
- Message visible in receive-only inbox
- CTA host is staging
- Delete QA user
Incident: “Users say welcome never arrived”
Triage order: app logs → queue → ESP dashboard → recipient junk → MIME content. Do not start by rewriting templates. Measure each hop.
Environment matrix
| Environment | Mailer | Assert how |
|---|---|---|
| phpunit | array / fake | Mail::fake |
| local | log or Mailpit | Inspect UI |
| staging | real SMTP sandbox | Receive-only inbox |
| production | provider | Metrics only, no QA probes to prod customers |
Never point automated tests at production customer mail flows.
Idempotency of welcome sends
If users refresh registration confirmation, do they get three welcomes? Assert once-per-user with a welcome_sent_at column or equivalent. Duplicate welcomes annoy and hurt ESP reputation.
Content security
Welcome mails that include unsigned temporary login tokens in URLs should expire quickly and land only on HTTPS. Assert token TTL in unit tests where possible.
Observability
Log message_id, user_id, and mailer driver on send. When someone files “welcome missing,” correlate without reading message bodies in shared slack.
Pairing with browser tests
Selenium can complete registration and wait on a mail harness (identity isolation). Keep Laravel fakes for speed; keep one coupled path for confidence.
Release gate proposal
Before promoting a build that touches registration:
- Unit/feature mail fakes green in CI
- Staging smoke receive succeeds once
- No failed jobs for
WelcomeMailin the last hour of soak - Template screenshots attached to the PR for HTML changes
Skipping (2) is the most common cause of “but tests passed” incidents. Mailby Quick Inbox or developers can serve as the receive side for (2) without polluting personal mail.
Common Laravel gotchas checklist
- Using
Mail::sendin a constructor - Forgetting
ShouldQueuewhile assuming async - Rendering markdown mail with undefined
$uservariables - Hard-coding
Fromthat fails SPF - Testing only happy path registration
Walk this list during code review on any PR touching welcome mail.
Walkthrough: debugging a missing welcome in staging
- Reproduce registration with a unique QA email.
- Confirm
usersrow exists. - Check
jobs/ Horizon for pending mail. - Check
failed_jobsfor exceptions rendering the mailable. - Check ESP acceptance logs for 250 vs 550.
- Check the receive-only inbox and staging user’s personal junk.
- Only then change template code.
Most “template bugs” die at step 3 or 4. Keep this order on an index card near the on-call laptop. When using Mailby as the receive target, note the session expiry so step 6 happens inside the lease.
Documentation debt
Update your internal README with the mailer env vars and the smoke receive procedure. Future teammates should not invent a new path that sends welcome mail to production customers by accident.
Treat welcome mail as a production dependency equal to payments: owned, monitored, and tested on every release train that touches registration. Shortcuts here become customer-trust incidents tomorrow.
Ownership note
Assign a named engineer as mailer owner for each app. Orphaned SMTP configs are how staging silently points at the wrong provider for months. The owner reviews welcome-mail PRs and keeps the smoke-receive checklist current when Mailby or the ESP workflows change.
Conclusion
Prove welcome email delivery in Laravel in layers: fake the generator in CI, smoke-test SMTP to a mailbox you control, and clean up identities afterward. Quick Inbox is a practical manual receive target; the developers surface supports programmatic receive when you design for it—without replacing framework mail fakes.
For inbound DNS issues after you deploy the receiving side of your own stack, see DNS cache after migration runbook.
Try it on Mailby
Open a receive-only disposable inbox when a short-lived address fits the job — session-bound, with timed purge.
