Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

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

FakeWhat 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 --parallel

Common 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.

Thanks for reading!

Donate to the site — we work hard for you.