Mastering Test Doubles and Fakes in Laravel: A Comprehensive Guide
Beyond the Vocabulary: Understanding Test Doubles
Testing often feels like a steep mountain to climb because of the dense academic language surrounding it.
- Dummies: These are placeholders. You pass them into functions to satisfy parameter lists, but you never actually use their values.
- Fakes: These have real working implementations but take shortcuts. A classic example is using an in-memory database instead of a full MySQLinstance.
- Stubs: These provide "canned answers." They respond to specific calls with pre-defined data but don't care about anything else.
- Spies: Think of these as stubs that take notes. They record what happened so you can verify it later.
- Mocks: The strictest form. They are pre-programmed with expectations and will actively throw an exception if they receive a call they didn't expect.
The Power of Laravel Facade Fakes
For years,
When you call Mail::fake(), you aren't just ignoring emails. You are swapping the real mailer in the service container with a MailFake instance. This fake records every mailable sent, allowing you to make powerful assertions without actually hitting an SMTP server.
public function test_post_store_sends_welcome_email()
{
Mail::fake();
// Perform the action
$this->post('/posts', ['title' => 'New Post']);
// Assert the mailable was sent to the right person
Mail::assertQueued(NewPostMailable::class, function ($mail) {
return $mail->hasTo('[email protected]');
});
}
Preventing the "Stray Request" Foot-Gun
One of the most dangerous aspects of testing is the "stray request." This happens when your test accidentally hits a real production API or sends a real email because you forgot to fake a specific service. preventStrayRequests().
By adding Http::preventStrayRequests() to your base test case's setUp method, the framework will throw an exception the moment any code attempts to make an external request that hasn't been explicitly faked. This turns your fake into a mock, providing a "tracer bullet" that illuminates hidden dependencies in your code. It’s a best practice that ensures your tests stay fast, deterministic, and isolated from the outside world.
Advanced Techniques: Real-Time Facades and Mockery
Sometimes you need to test a custom service class that doesn't have a built-in
Using the Mock Helper
Laravel's mock() helper streamlines the process of creating a
$this->mock(LanguageAI::class, function ($mock) {
$mock->shouldReceive('analyze')->once()->andReturn(true);
});
The Real-Time Facade Magic
If you prefer the clean, static syntax of facades but don't want to create a dedicated Facade class, you can use a real-time facade. By simply prefixing your import with Facades\,
use Facades\App\Services\LanguageAI;
// In your code or test:
LanguageAI::expects('analyze')
->with('post body')
->andReturn(true);
Critical Distinction: ShouldReceive vs. Expects
A common mistake is using shouldReceive() when you actually mean expects(). The difference is vital for test integrity.
shouldReceive()is permissive. If the method is never called, the test still passes. This leads to "false confidence"—you think you're testing a feature that isn't actually running.expects()is strict. It acts as a true mock. If the method isn't called exactly as specified, the test fails.
Always lean toward expects() to ensure your code is actually executing the logic you intend to verify. Being strict in your tests leads to total confidence in your production deployments.
