Testing facades — Facade::fake(), spy(), shouldReceive()
Concept
Testing code that uses Facades is straightforward because Laravel provides first-class support for faking, spying on, and mocking Facades in tests.
Facade::fake(): Replaces the Facade's underlying instance with a fake that records calls but doesn't execute real logic. Used for side-effect facades (Mail, Notification, Event, Job, Storage).
Facade::spy(): Like fake() but also tracks calls for later assertion. Useful when you want to assert after the fact rather than setting up expectations before.
Facade::shouldReceive('method'): Uses Mockery to set up expectations. Returns a Mockery expectation object for fluent configuration: .once(), .times(2), .andReturn(value), .with(args).
Common Facade fakes:
Mail::fake()— prevents emails from sending. Assert withMail::assertSent().Notification::fake()— prevents notifications. Assert withNotification::assertSentTo().Event::fake()— prevents listeners from running. Assert withEvent::assertDispatched().Bus::fake()— prevents jobs from dispatching. Assert withBus::assertDispatched().Storage::fake('disk')— creates a temporary in-memory disk. Assert withStorage::disk()->assertExists().Http::fake()— stubs HTTP requests. Assert withHttp::assertSent().Queue::fake()— prevents queued jobs. Assert withQueue::assertPushed().
Partial fakes: Mail::fake([OrderConfirmation::class]) — only fake specific mailable classes, let others send normally.
Code Example
<?php
namespace Tests\Feature;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Http;
use App\Mail\OrderConfirmation;
use App\Events\OrderPlaced;
class OrderTest extends \Illuminate\Foundation\Testing\TestCase
{
public function test_order_sends_confirmation_email(): void
{
Mail::fake(); // prevents real email sending
$user = User::factory()->create();
$this->actingAs($user)->postJson('/orders', [...]);
// Assert the mailable was sent to the right address
Mail::assertSent(OrderConfirmation::class, function(OrderConfirmation $mail) use ($user) {
return $mail->hasTo($user->email);
});
Mail::assertSent(OrderConfirmation::class, 1); // sent exactly once
Mail::assertNotSent(\App\Mail\ShippingUpdate::class); // this was NOT sent
}
public function test_order_dispatches_event(): void
{
Event::fake([OrderPlaced::class]); // only fake this event, others fire normally
$this->postJson('/orders', [...]);
Event::assertDispatched(OrderPlaced::class, function(OrderPlaced $event) {
return $event->order->status === 'pending';
});
}
public function test_invoice_uploaded_to_storage(): void
{
Storage::fake('s3'); // in-memory disk, no AWS calls
$this->postJson('/invoices', ['order_id' => 1]);
Storage::disk('s3')->assertExists('invoices/1.pdf');
Storage::disk('s3')->assertMissing('invoices/temp.pdf');
}
public function test_external_api_called(): void
{
Http::fake([
'api.example.com/*' => Http::response(['status' => 'ok'], 200),
]);
$this->postJson('/sync');
Http::assertSent(fn($request) => $request->url() === 'https://api.example.com/sync');
}
}