0

Testing facades — Facade::fake(), spy(), shouldReceive()

Intermediate5 min read·lv-04-005

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 with Mail::assertSent().
  • Notification::fake() — prevents notifications. Assert with Notification::assertSentTo().
  • Event::fake() — prevents listeners from running. Assert with Event::assertDispatched().
  • Bus::fake() — prevents jobs from dispatching. Assert with Bus::assertDispatched().
  • Storage::fake('disk') — creates a temporary in-memory disk. Assert with Storage::disk()->assertExists().
  • Http::fake() — stubs HTTP requests. Assert with Http::assertSent().
  • Queue::fake() — prevents queued jobs. Assert with Queue::assertPushed().

Partial fakes: Mail::fake([OrderConfirmation::class]) — only fake specific mailable classes, let others send normally.

Code Example

php
<?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');
    }
}