Tests: factories, fakes and Pest
A real test must not send emails, charge money or depend on other people's services. Laravel has factories that create data in one line, and fakes that intercept emails, jobs and requests and let you check that they were sent.
Factories
// database/factories/PostFactory.php
public function definition(): array
{
return [
'user_id' => User::factory(),
'title' => fake()->sentence(),
'body' => fake()->paragraphs(3, true),
'published_at' => now(),
];
}
public function draft(): static
{
return $this->state(['published_at' => null]);
}$post = Post::factory()->create();
$drafts = Post::factory()->count(3)->draft()->create();
$mine = Post::factory()->for($user)->create(['title' => 'My post']);The draft() method is a state: a named variant of the data.
A clean database
The RefreshDatabase trait returns the database to its initial state after every test:
use Illuminate\Foundation\Testing\RefreshDatabase;
class PostTest extends TestCase
{
use RefreshDatabase;
public function test_guest_does_not_see_drafts(): void
{
Post::factory()->draft()->create(['title' => 'Secret']);
$this->get('/posts')->assertOk()->assertDontSee('Secret');
}
}A logged-in user
$user = User::factory()->create();
$this->actingAs($user)
->post('/posts', ['title' => 'New post', 'body' => 'Text'])
->assertRedirect();
$this->assertDatabaseHas('posts', ['title' => 'New post', 'user_id' => $user->id]);Faking mail
use Illuminate\Support\Facades\Mail;
Mail::fake();
$this->post('/orders', $data);
Mail::assertSent(OrderShipped::class, fn ($mail) => $mail->hasTo('anna@example.com'));The email is not sent, yet the test knows there was an attempt to send it.
Other fakes
| Fake | What it checks |
|---|---|
Queue::fake() | Queue::assertPushed(ProcessVideo::class) |
Notification::fake() | Notification::assertSentTo($user, InvoicePaid::class) |
Event::fake() | Event::assertDispatched(OrderPlaced::class) |
Storage::fake('public') | Storage::disk('public')->assertExists('a.jpg') |
Http::fake() | Http::assertSent(...) |
Uploading a file
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
Storage::fake('public');
$this->actingAs($user)->post('/avatar', [
'avatar' => UploadedFile::fake()->image('me.jpg', 200, 200),
])->assertRedirect();
Storage::disk('public')->assertExists('avatars/' . $user->id . '.jpg');Time
$this->travelTo(now()->addDays(31));
$this->assertTrue($subscription->fresh()->isExpired());Pest
Pest is another way to write the same tests, without classes:
// tests/Feature/PostTest.php
use App\Models\Post;
it('shows published posts', function () {
$post = Post::factory()->create();
$this->get('/posts')->assertOk()->assertSee($post->title);
});
it('counts words', function () {
expect(str('one two three')->wordCount())->toBe(3);
});New Laravel projects offer a choice between Pest and PHPUnit on creation.
Running
php artisan test
php artisan test --filter=PostTest
php artisan test --parallelCommon mistake
A fake is called before the action, not after. If you write Mail::fake() after the request, the email has already been sent for real, and assertSent finds nothing. And do not fake the thing you are checking: a test with Event::fake() does not run the listeners, so it does not verify their work.