- Knowledge
- technology
- OOP
- Tips
- Programming
- Tips
- Tutorial
- SEO
- Ranking
- Knowledge
- Special Day
- Seo
- Bug
- Data science
- Seo
- artificial intelligence
- Machine Learning
- Robotics
- happyNewYear2021
- newYearEve
- 2021
- Automation
- Smart Home
- Career
- Best Practices
- Git
- Logging
- Web Fundamentals
- DNS
- HTTPS
- Performance
- AI Tools
- ChatGPT
- Claude
- Gemini
- Laravel
- Eloquent
- MySQL
- HTTPS
- TLS
- Web Security
- Certificates
- Developer Life
- Debugging
- Docker
- DevOps
- Transactions
- Queues
- LLMs
- AI
- AI Coding
- Developer Tools
- React Native
- Expo
- Kate PMS
- Mobile Apps
- Laravel
- Authentication
- Sanctum
- Cookies
- API Design
- Payments
- Idempotency
- DeepSeek
- Open Source AI
- LLMs
- AI News
- Git
- Version Control
- AI Coding
- Prompting
- PHP
- Checklist
- MCP
- AI Agents
- OpenAI
- Architecture
- Microservices
- Modular Monolith
- Estimation
- Developer Life
- Project Planning
- Humour
- OAuth
- OpenID Connect
- Authentication
- Embeddings
- Vector Search
- RAG
- pgvector
- OpenAI
- GPT-4.1
- Codex CLI
- Events
- Testing
- Clean Code
- Maintainability
- Code Review
- Webhooks
- API
- Security
- Claude Code
- Workflow
- AI
- LLM
- Prompt Injection
- Mobile
- React
- Networking
- TCP
- UDP
- HTTP/3
- CLAUDE.md
- AWS
- Cloud Security
- Backups
- PHPUnit
- Software Engineering
- Leadership
- Communication
- RAG
- Embeddings
- AI Engineering
- IT Infrastructure
- Networking
- Access Control
- CI/CD
- GitHub Actions
- Gemini CLI
- Claude Code
- JavaScript
- Async/Await
- Node.js
- Promises
- Security
- Cryptography
- Passwords
- MySQL
- Database
- Vibe Coding
- Software Quality
- DNS
- Code Reading
- Onboarding
- Productivity
- Background Jobs
- Developer Humour
- Estimates
- Dev Life
- JWT
- o3-mini
- DeepSeek R1
- Rate Limiting
- Kate PMS
- E-Signing
- Audit Trail
- REST
- GraphQL
- API Design
- Laravel 12
- Upgrade Guide
- Open Source
- Self-Hosting
- Task Scheduling
- Cron
- Secrets
- CORS
- PHP
- PHP-FPM
- OPcache
- GitHub Copilot
- Software Architecture
- Engineering
- TypeScript
- JavaScript
- Type Safety
- AI Security
- React Native
- Product Design
- AI Agents
- Kiro
- Queues
- Redis
- RabbitMQ
- AWS SQS
- Nginx
- Apache
- GPT-5
- gpt-oss
- Clean Code
- Architecture
- Naming
- Documentation
- Career
- ADR
- Teamwork
- Supply Chain
- Kate HRM
- HR Software
- Permissions
- System Design
- Pagination
- SSH
- Linux
- Big O
- Databases
- Laravel Boost
- MCP
- Developer Skills
- Validation
- Databases
- Indexes
- Code Quality
- Deployment
- Developer Humour
- Feature Flags
- Code Review
- Pull Requests
- Docker
- Cursor
- Authorization
- RBAC
- Gemini
- Long Context
- PHP 8.4
- Caching
- Dependency Injection
- Web Performance
- Browser
- CSS
- Database
- Migrations
- ChatGPT
- AI for Developers
- Monitoring
- On-Call
- REST
- Backend
- SQL
- NoSQL
- Database Design
- Coding Agents
- Claude 4
- API Resources
- REST API
- Load Balancing
- Scaling
- AWS
- AI Tools
- Claude
- Sora 2
- CTE
- 2FA
- TOTP
- Programming Languages
- Prompts
- Developer Workflow
- API Gateway
- APIs
- Passport
- API Auth
- Learning
- Burnout
- Developer Growth
- Web Development
- SEO
- Kate Mall
- ChatGPT Atlas
- Agent Skills
- Middleware
- Laravel 12
- Collections
- Context Window
- Monitoring
- Commit Messages
- Self Review
- Growth
- Regex
- Programming Basics
- Text Processing
- Database Design
- Normalization
- Linux
- Server Security
- Linux Foundation
- Open Standards
- Legacy Code
- Documentation
- AI Workflow
- File Uploads
- Test Data
- Hashing
- Performance
- Caching
- Enums
- Scope Creep
- Estimation
- Codex
- Gemini CLI
- Timezones
- Carbon
- Bugs
- PHP 8.5
- Gemini 3
- GPT-5.1
- Data Integrity
- Event Loop
- Async
- Opus 4.5
- AI Models
- React
- Forms
- Frontend
- Backups
- AI Images
- DALL-E
- Midjourney
- Race Conditions
- Concurrency
- Legacy Code
- Refactoring
- Senior Engineer
- Scope
- LLM
- CDN
- Web
- Sub-Agents
- Soft Deletes
- Audit Log
- Concurrency
- AI Learning
- NestJS
- AI Evals
- Policies
- SPF DKIM DMARC
- Unicode
- UTF-8
- Knowledge Graph
- Value Objects
- Technical Debt
- Feature Flags
- Laravel Pennant
- Deployment
- Copilot
- Composer
- Dependencies
- Artisan
- Automation
- AWS S3
- Object Storage
- Cloud
- Small Language Models
- Ollama
- Production
- Sessions
- HTTP
- Mentoring
- SQL
- Virtual Machines
- Web Development
- HTTP/2
- QUIC
- Web Performance
- AI Integration
- LLM API
- SOLID
- OOP
- Hosting
- Serverless
- Merge Conflicts
- Temperature
- AI Development
- Reverse Proxy
- Nginx
- Infrastructure
- Verification
- Passkeys
- WebAuthn
- Teams
- Communication
- Stakeholders
- Monorepo
- CI/CD
- Versioning
- JSON Schema
- Livewire
- Inertia
- Meetings
- Distributed Systems
- Privacy
- Full-Stack
- T-Shaped Skills
- Money
- Notifications
- Web Security
- HTTP Headers
- CSP
- Function Calling
- Load Testing
- k6
- Data Extraction
- Debugging
- WebSockets
- SSE
- Real-Time
- Laravel Reverb
- Infrastructure as Code
- Terraform
- Side Projects
- Laravel Pint
- OpenAPI
- Swagger
- UX
- Multimodal
- Jest
- Pair Programming
- APIs
- Rate Limiting
- Resilience
- Dev Humour
- Design Tokens
- JWT
- API Keys
- Sessions
- PHPStan
- Rector
- Incidents
- Reporting
- Dashboards
- Zero Trust
- IAM
- Search
- Laravel Scout
- Junior Developers
- Mentoring
- Images
- WebP
- AVIF
- Bug Reports
- Let's Encrypt
- Design Docs
- Software Design
- Observers
- Replication
- Accountability
- Data Structures
- Reliability
- LLM Memory
- Error Handling
- Payments
- Payment Gateway
- Webhooks
- PCI DSS
- Observability
- OpenTelemetry
- Personal Brand
- Writing
- Conventions
- Dates
- Scheduling
- Disaster Recovery
- Compression
- Brotli
- Deadlines
- Developer Habits
- State Machines
- Tech Roles
- UUID
- ULID
- Horizon
- Planning
- Engineering Culture
- Ownership
- Soft Skills
- Socialite
- Cost Control
- Collations
- Unicode
- Octane
- PostgreSQL
Laravel Testing With Fakes: Mail, Queue, Http, Storage and Notifications
About Post
Somewhere out there is a staging database full of users called "Test Test", and an inbox belonging to a real customer who received forty password reset emails because someone ran the test suite against the wrong .env.
Tests that touch the outside world are slow, flaky and occasionally embarrassing. They send real emails, call real payment APIs, write real files to S3 and push real jobs onto real queues. And when the third-party sandbox is down, your build is red for reasons that have nothing to do with your code.
Laravel's fakes fix this with one line each. But they also make it very easy to write a test that passes for the wrong reason. Let's do both halves: how they work, and where they lie to you.
What a fake actually does
When you call Mail::fake(), Laravel swaps the real mailer behind the Mail facade for a fake object that records everything instead of sending it. Your application code doesn't change at all. It still calls Mail::to($user)->send(...); the fake just writes it down.
Then you ask the fake questions: was this mailable sent? To whom? How many times? That's the whole idea, and every fake below follows the same pattern: fake first, act, then assert.
A test that uses three of them
Here's a typical feature test for placing an order. The controller charges the card through an HTTP API and emails a receipt:
public function test_placing_an_order_charges_and_emails_a_receipt(): void
{
Mail::fake();
Http::preventStrayRequests();
Http::fake([
'api.payments.test/*' => Http::response(['id' => 'ch_123', 'status' => 'paid']),
]);
$user = User::factory()->create();
$data = ['product_id' => Product::factory()->create()->id, 'quantity' => 2];
$this->actingAs($user)
->post('/orders', $data)
->assertCreated();
Mail::assertSent(Receipt::class);
Http::assertSent(fn (Request $request) =>
$request->url() === 'https://api.payments.test/charges'
);
}
(Request here is Illuminate\Http\Client\Request, not the incoming HTTP request.)
Notice Http::preventStrayRequests(). Without it, any URL you didn't fake is sent for real. With it, an unexpected outgoing call throws an exception, and you find out that your code also calls a geocoding API you forgot about. I put it in every test that touches HTTP, or once in the base TestCase.
The fakes, one by one
Mail::fake()
Assert on the mailable class, and usually on the recipient too:
Mail::assertSent(Receipt::class, fn (Receipt $mail) => $mail->hasTo($user->email));
Mail::assertSent(Receipt::class, 1); // exactly once
Mail::assertNothingSent();
Queue::fake()
Records jobs instead of running them. Great for checking that slow work is pushed to the background:
Queue::fake();
// ... act ...
Queue::assertPushed(GenerateInvoicePdf::class, fn ($job) => $job->order->is($order));
Queue::assertNotPushed(SendSmsReminder::class);
If you dispatch chains or batches, Bus::fake() gives you assertions like Bus::assertChained() and Bus::assertBatched().
Http::fake()
Beyond the happy path, fake the failures. That's where the interesting bugs live. A sequence returns different responses on each call, perfect for testing retries:
Http::fake([
'api.payments.test/*' => Http::sequence()
->push('Server Error', 500)
->push(['id' => 'ch_123', 'status' => 'paid'], 200),
]);
Storage::fake()
Replaces a disk with a temporary local one, so uploads never reach S3:
Storage::fake('s3');
$photo = UploadedFile::fake()->image('leak.jpg');
$this->actingAs($tenant)->post('/maintenance-requests', [
'description' => 'Kitchen tap is leaking',
'photo' => $photo,
]);
Storage::disk('s3')->assertExists('maintenance/'.$photo->hashName());
That path assumes the controller calls $request->file('photo')->store('maintenance', 's3'), which names the file by its hash.
Notification::fake()
Works per notifiable, and the callback can check which channels were used:
Notification::assertSentTo($tenant, RentReminder::class,
fn ($notification, array $channels) => in_array('mail', $channels)
);
The gotchas that make tests lie
Fakes are so convenient that it's easy to forget what they switch off. These are the ones I see most.
Queue::fake() means your jobs never run
If the receipt email is sent from inside a queued job, then Queue::fake() plus Mail::assertSent() fails, because the job that sends the mail never executes. That's correct behaviour, not a bug. Split it into two tests: one asserts the job was pushed, the other runs the job directly and asserts what it does.
Queued mailables need assertQueued
If Receipt implements ShouldQueue, the fake records it as queued, and Mail::assertSent() fails. Use Mail::assertQueued() instead. The error message is clear once you know; the first time, it's confusing.
Event::fake() switches off model events too
Call Event::fake() with no arguments and every event is faked, including Eloquent's. If a model sets a UUID or slug in a creating hook, your factories suddenly produce models without one. Fake only what you need: Event::fake([OrderShipped::class]).
A fake only knows what you told it
Http::fake() returns whatever JSON you wrote. If the real API's response is shaped differently, your tests pass and production breaks. Base your fake responses on real responses from the provider's sandbox or docs, and keep them in fixture files so they're easy to update.
A good habit: for every assertSent, ask what the negative test looks like. Invalid order? Mail::assertNothingSent(). Payment declined? Queue::assertNotPushed(GenerateInvoicePdf::class). The side effects that must not happen are often the ones that matter most.
Where fakes stop being enough
Fakes test that your code asked for the right side effect. They don't test that the email template renders, that the S3 bucket policy allows the upload, or that the payment provider accepts your payload. For those, render mailables in their own tests ($mailable->assertSeeInHtml(...)), and keep a small number of real integration checks against sandboxes, run separately from the fast suite.
The fast suite stays fast, offline and deterministic. That's the deal.
Quick checklist
- Fake before you act, assert after.
Http::preventStrayRequests()everywhere.- Test failure responses, not only success.
- Remember what
Queue::fake()andEvent::fake()switch off. - Write the negative assertion too.
Which fake has caught you out the most? My bet is the queued mailable: almost everyone stares at a failing assertSent at least once before spotting ShouldQueue.

Be first to comment it...