Faking Laravel services — Mail, Event, Queue, Storage, Http
Concept
Testing Jobs, Events, and Notifications uses fake facades to verify dispatch without actually running external operations (sending emails, hitting queues, making HTTP calls).
Why fake instead of real: Tests should be fast and deterministic. Mail servers are slow. Queue workers aren't running during tests. External APIs fail intermittently. Fakes let you verify "the right thing was dispatched with the right data" without executing the operation.
Bus::fake(): Fakes job dispatching. Jobs are NOT executed. Assertions verify jobs were dispatched.
Bus::assertDispatched(ProcessOrder::class).Bus::assertDispatchedSync(ProcessOrder::class)— fordispatchSync().Bus::assertBatched()— for job batches.Bus::assertChained()— for job chains.Bus::assertNothingDispatched().
Notification::fake():
Notification::assertSentTo($user, OrderConfirmation::class).Notification::assertSentTo($user, OrderConfirmation::class, fn($n) => $n->order->id === 1).Notification::assertNotSentTo($user, ...).Notification::assertCount(3)— total notifications sent.
Event::fake() scoped: Event::fake([OrderPlaced::class]) — only fake specific events. Other events fire normally.
Testing real listener execution: When you need to verify a listener actually processes an event, DON'T fake that event. Fire the event and assert the side effect in the database.
Http::fake(): Stub outgoing HTTP requests (from Http::get(), Http::post(), etc.).
Code Example
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;
class OrderProcessingTest extends TestCase
{
use RefreshDatabase;
public function test_placing_order_dispatches_processing_job(): void
{
Bus::fake();
$user = User::factory()->create();
$this->actingAs($user)->postJson('/api/orders', ['product_id' => 1, 'quantity' => 2]);
Bus::assertDispatched(\App\Jobs\ProcessOrder::class, function($job) {
return $job->quantity === 2;
});
}
public function test_order_processed_sends_confirmation_notification(): void
{
Notification::fake();
$user = User::factory()->create();
$order = Order::factory()->for($user)->create();
(new \App\Jobs\ProcessOrder($order))->handle();
Notification::assertSentTo($user, \App\Notifications\OrderConfirmation::class,
fn($n) => $n->order->id === $order->id
);
}
public function test_order_fires_correct_events(): void
{
Event::fake([\App\Events\OrderPlaced::class]); // only fake this event
$user = User::factory()->create();
$this->actingAs($user)->postJson('/api/orders', ['product_id' => 1]);
Event::assertDispatched(\App\Events\OrderPlaced::class);
}
public function test_payment_gateway_called_with_correct_amount(): void
{
Http::fake([
'api.paymentgateway.com/*' => Http::response(['status' => 'success'], 200),
]);
$order = Order::factory()->create(['total' => 99.99]);
(new \App\Services\PaymentService)->charge($order);
Http::assertSent(function(\Illuminate\Http\Client\Request $request) {
return $request->url() === 'https://api.paymentgateway.com/charge'
&& $request->data()['amount'] === 9999; // cents
});
}
public function test_job_chain_dispatched_in_order(): void
{
Bus::fake();
$order = Order::factory()->create();
\App\Jobs\FulfillOrder::dispatch($order);
Bus::assertChained([
\App\Jobs\ChargePayment::class,
\App\Jobs\UpdateInventory::class,
\App\Jobs\SendConfirmationEmail::class,
]);
}
}