Fakes
Mail, Queue, Event, Storage and Http fakes record what your code tried to do instead of doing it, so tests assert intent.
Mail::fake(); Mail::assertSent(OrderPlaced::class, $callback = null);A fake replaces the implementation behind a facade, in the service container, with a double that records calls instead of performing them. After Mail::fake() no SMTP connection is opened; after Queue::fake() no job reaches Redis. Your test then asserts what the code tried to do. This is not only about speed: a test that touches the network fails when someone else's API is down, and can send a real email to a real person.
The five you will reach for
Mail::fake(), thenMail::assertSent(OrderPlaced::class, $callback = null),assertNotSent,assertNothingSent,assertSentCount(3). A mailable that is queued is asserted withassertQueued, notassertSent.Queue::fake(), thenQueue::assertPushed(ShipOrder::class),assertPushedOn('shipping', ShipOrder::class),assertNotPushed,assertNothingPushed. Chains and batches are asserted through the Bus facade:Bus::fake()withBus::assertChained([...])orBus::assertBatched(...).Event::fake(), thenEvent::assertDispatched(OrderShipped::class),assertNotDispatched,assertNothingDispatched, andEvent::assertListening($event, $listener)for the wiring itself.Storage::fake('photos')swaps that disk for a temporary one, thenStorage::disk('photos')->assertExists('photo1.jpg'),assertMissing,assertCount('/wallpapers', 2).Http::fake([...])stubs outgoing requests,Http::assertSent(fn (Request $request) => ...)inspects them, andHttp::preventStrayRequests()turns any unfaked request into an exception.
Fake less than everything
A total fake can hide behaviour you wanted to keep. Queue::fake([ShipOrder::class]) fakes only that job; Queue::fake()->except([ShipOrder::class]) is the inverse, and Queue::fakeExcept([ShipOrder::class]) is the same thing in one call. Event::fake()->except([...]) does it for events, and Event::fakeFor(fn () => ...) fakes only inside a closure.
Example
<?php
// tests/Feature/CheckoutTest.php
declare(strict_types=1);
namespace Tests\Feature;
use App\Jobs\ChargeCard;
use App\Mail\OrderPlaced;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Queue;
use Tests\TestCase;
class CheckoutTest extends TestCase
{
use RefreshDatabase;
public function test_checking_out_queues_the_charge_and_mails_a_receipt(): void
{
$user = User::factory()->create(['email' => '[email protected]']);
// Fake before the code under test runs, never after.
Queue::fake();
Mail::fake();
$this->actingAs($user)
->post('/checkout', ['sku' => 'LARAVEL-13', 'quantity' => 1])
->assertRedirect('/orders');
Queue::assertPushed(ChargeCard::class, function (ChargeCard $job) use ($user) {
return $job->order->user_id === $user->id;
});
Mail::assertSent(OrderPlaced::class, function (OrderPlaced $mail) use ($user) {
return $mail->hasTo($user->email);
});
// Mail::assertSent(OrderPlaced::class, 1) is the same assertion.
Mail::assertSentOnce(OrderPlaced::class);
}
}
When to use it
- Assert that placing an order queues the charge job without running a worker.
- Prove a receipt is addressed to the right customer without sending mail.
- Test an upload route with no filesystem disk and no leftover files.
- Keep a test that talks to a payment or shipping API off the network.
More examples
Stubbing an outgoing API call
Http::fake() matches URL patterns and Http::response() builds the body and status. Call preventStrayRequests() in every test that uses the HTTP client, or in your base TestCase: without it, a URL you forgot to fake makes a real request and the suite becomes dependent on someone else's uptime.
<?php
// tests/Feature/ShippingRateTest.php
declare(strict_types=1);
namespace Tests\Feature;
use Illuminate\Http\Client\Request;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;
class ShippingRateTest extends TestCase
{
public function test_the_quoted_rate_comes_from_the_carrier(): void
{
// Any request that is not faked now throws instead of leaving the machine.
Http::preventStrayRequests();
Http::fake([
'api.carrier.test/rates*' => Http::response(['amount' => 1250], 200),
]);
$this->postJson('/quotes', ['postcode' => 'SW1A 1AA'])
->assertOk()
->assertJsonPath('data.amount', 1250);
Http::assertSent(function (Request $request) {
return $request->url() === 'https://api.carrier.test/rates'
&& $request['postcode'] === 'SW1A 1AA';
});
}
}
An upload, with no real disk
UploadedFile::fake()->image() generates a real image file of the given size, so validation rules such as image and dimensions still run. Storage::fake() points the named disk at a temporary directory that is cleared for you, which is what keeps a passing suite from filling storage/app.
<?php
// tests/Feature/AvatarUploadTest.php
declare(strict_types=1);
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;
class AvatarUploadTest extends TestCase
{
use RefreshDatabase;
public function test_an_avatar_is_stored_on_the_avatars_disk(): void
{
Storage::fake('avatars');
$user = User::factory()->create();
$file = UploadedFile::fake()->image('me.jpg', 200, 200);
$this->actingAs($user)
->post('/profile/avatar', ['avatar' => $file])
->assertRedirect('/profile');
Storage::disk('avatars')->assertExists("{$user->id}/me.jpg");
Storage::disk('avatars')->assertMissing('me.png');
}
}
Faking one event and leaving the rest alone
A partial fake keeps every other listener and observer running, so the rest of the request still behaves like production. The ordering is the part that bites: a full Event::fake() placed above the factory call would stop model events and the row could come out without the fields an observer sets.
public function test_publishing_dispatches_the_published_event(): void
{
// Factories run first: model events still fire, so UUIDs and
// observers behave normally while the rows are created.
$post = Post::factory()->create();
Event::fake([PostPublished::class]);
$this->actingAs($post->user)->post("/posts/{$post->id}/publish");
Event::assertDispatched(PostPublished::class, function (PostPublished $event) use ($post) {
return $event->post->is($post);
});
}
Discussion