Laravel

Testing

Laravel is built with testing in mind, providing seamless integration with both PHPUnit and Pest. Feature tests, unit tests, database testing, and HTTP assertion helpers make writing tests straightforward.

PHPUnit vs Pest: Laravel supports both PHPUnit (class-based) and Pest (functional, expressive syntax). Pest is installed by default in newer Laravel projects and wraps PHPUnit under the hood.
📄Terminal – Setup & Running
BASH
# Install Pest (if not already installed)
composer require pestphp/pest --dev --with-all-dependencies
php artisan pest:install

# Run all tests
php artisan test

# Run tests with coverage (requires Xdebug or PCOV)
php artisan test --coverage

# Run a specific test file
php artisan test tests/Feature/PostTest.php

# Run tests matching a filter
php artisan test --filter='PostTest::it_can_create_a_post'

# Run in parallel (faster for large test suites)
php artisan test --parallel

# PHPUnit directly
./vendor/bin/phpunit
./vendor/bin/pest
Feature Test – HTTP Requests (PHPUnit Style): Feature tests test the entire application stack including routing, middleware, controllers, and responses.
📄tests/Feature/PostTest.php (PHPUnit)
PHP
<?php

namespace Tests\Feature;

use Tests\TestCase;
use App\Models\User;
use App\Models\Post;
use Illuminate\Foundation\Testing\RefreshDatabase;

class PostTest extends TestCase
{
    use RefreshDatabase;

    public function test_authenticated_user_can_view_posts(): void
    {
        $user = User::factory()->create();
        Post::factory()->count(3)->create(['user_id' => $user->id]);

        $response = $this->actingAs($user)->get('/posts');

        $response->assertStatus(200);
        $response->assertViewIs('posts.index');
        $response->assertViewHas('posts');
    }

    public function test_user_can_create_a_post(): void
    {
        $user = User::factory()->create();

        $response = $this->actingAs($user)->post('/posts', [
            'title'   => 'My First Post',
            'content' => 'Hello world! This is my post.',
        ]);

        $response->assertRedirect('/posts');
        $this->assertDatabaseHas('posts', [
            'title'   => 'My First Post',
            'user_id' => $user->id,
        ]);
    }

    public function test_post_creation_requires_title(): void
    {
        $user = User::factory()->create();

        $response = $this->actingAs($user)->post('/posts', [
            'content' => 'Missing title here.',
        ]);

        $response->assertSessionHasErrors('title');
        $response->assertStatus(302);
    }

    public function test_guest_cannot_create_post(): void
    {
        $response = $this->post('/posts', ['title' => 'Test']);

        $response->assertRedirect('/login');
    }
}
Feature Test – Pest Style: Pest uses a functional, expressive API. The same tests as above written in Pest syntax are cleaner and more readable.
📄tests/Feature/PostTest.php (Pest)
PHP
<?php

use App\Models\User;
use App\Models\Post;

uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);

it('shows posts to authenticated user', function () {
    $user = User::factory()->create();
    Post::factory()->count(3)->create(['user_id' => $user->id]);

    $this->actingAs($user)
        ->get('/posts')
        ->assertStatus(200)
        ->assertViewIs('posts.index')
        ->assertViewHas('posts');
});

it('creates a post successfully', function () {
    $user = User::factory()->create();

    $this->actingAs($user)
        ->post('/posts', ['title' => 'Pest Post', 'content' => 'Content here'])
        ->assertRedirect('/posts');

    expect(Post::where('title', 'Pest Post')->exists())->toBeTrue();
});

it('requires a title to create a post', function () {
    $user = User::factory()->create();

    $this->actingAs($user)
        ->post('/posts', ['content' => 'No title'])
        ->assertSessionHasErrors('title');
});

it('redirects guests to login', function () {
    $this->post('/posts', ['title' => 'Test'])
        ->assertRedirect('/login');
});
Database Testing with RefreshDatabase: Use the RefreshDatabase trait to reset the database state between tests. Use DatabaseTransactions if you prefer rollback over migration.
📄tests/Feature/UserTest.php
PHP
<?php

namespace Tests\Feature;

use Tests\TestCase;
use App\Models\User;
use App\Models\Post;
use Illuminate\Foundation\Testing\RefreshDatabase;

class UserTest extends TestCase
{
    // Runs all migrations fresh before each test
    use RefreshDatabase;

    // Or use this to wrap each test in a transaction (faster, no re-migration)
    // use DatabaseTransactions;

    public function test_user_can_be_created(): void
    {
        $user = User::factory()->create(['name' => 'John Doe']);

        // Assert record exists in the database
        $this->assertDatabaseHas('users', ['name' => 'John Doe']);
        $this->assertDatabaseCount('users', 1);
    }

    public function test_user_deletion(): void
    {
        $user = User::factory()->create();
        $user->delete();

        // Assert record is gone
        $this->assertDatabaseMissing('users', ['id' => $user->id]);

        // For soft deletes, use assertSoftDeleted
        // $this->assertSoftDeleted('users', ['id' => $user->id]);
    }

    public function test_user_has_posts(): void
    {
        $user = User::factory()
            ->has(Post::factory()->count(5))
            ->create();

        $this->assertCount(5, $user->posts);
    }
}
Model Factories: Factories generate fake model data for testing. Define your factories in database/factories/.
📄database/factories/PostFactory.php
PHP
<?php

namespace Database\Factories;

use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;

class PostFactory extends Factory
{
    public function definition(): array
    {
        return [
            'user_id'    => User::factory(),
            'title'      => fake()->sentence(),
            'content'    => fake()->paragraphs(3, true),
            'published'  => fake()->boolean(80), // 80% chance of true
            'published_at' => fake()->optional()->dateTimeThisYear(),
        ];
    }

    // Named state: Post that is published
    public function published(): static
    {
        return $this->state(fn (array $attributes) => [
            'published'    => true,
            'published_at' => now(),
        ]);
    }

    // Named state: Post that is a draft
    public function draft(): static
    {
        return $this->state(fn (array $attributes) => [
            'published'    => false,
            'published_at' => null,
        ]);
    }
}

// --- Usage in tests ---
// Post::factory()->create();                   // Single post
// Post::factory()->count(10)->create();        // 10 posts
// Post::factory()->published()->create();      // Published post
// Post::factory()->draft()->make();            // Draft post (not persisted)
Mocking with Facades: Laravel provides a convenient fake() / facade mocking system. Use Mail::fake(), Event::fake(), Notification::fake(), etc. to assert without actually sending.
📄tests/Feature/RegistrationTest.php
PHP
<?php

namespace Tests\Feature;

use Tests\TestCase;
use App\Models\User;
use App\Mail\WelcomeMail;
use App\Events\UserRegistered;
use App\Notifications\WelcomeNotification;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Notification;

class RegistrationTest extends TestCase
{
    use RefreshDatabase;

    public function test_welcome_email_is_sent_on_registration(): void
    {
        Mail::fake(); // Intercept all mail

        $this->post('/register', [
            'name'                  => 'Jane Doe',
            'email'                 => 'jane@example.com',
            'password'              => 'password',
            'password_confirmation' => 'password',
        ]);

        $user = User::where('email', 'jane@example.com')->first();

        // Assert that a specific mailable was sent
        Mail::assertSent(WelcomeMail::class, function ($mail) use ($user) {
            return $mail->hasTo($user->email);
        });
    }

    public function test_user_registered_event_fires(): void
    {
        Event::fake(); // Intercept all events

        $user = User::factory()->create();
        event(new UserRegistered($user));

        Event::assertDispatched(UserRegistered::class, function ($event) use ($user) {
            return $event->user->id === $user->id;
        });
    }

    public function test_welcome_notification_is_sent(): void
    {
        Notification::fake(); // Intercept all notifications

        $user = User::factory()->create();
        $user->notify(new WelcomeNotification);

        Notification::assertSentTo($user, WelcomeNotification::class);
    }
}