How to Test Laravel Applications with Pest: A Complete Guide

We earn commissions when you shop through the links below.

If you’ve been putting off writing tests for your Laravel projects, learning how to test Laravel applications with Pest might be the nudge you need. Pest is a PHP testing framework built on top of PHPUnit that strips away the ceremony and lets you write expressive, readable tests in a fraction of the time. In this guide I’ll walk you through setup, core concepts, and practical examples you can drop into a real project today.

Why Pest Over PHPUnit?

PHPUnit is battle-tested and perfectly fine. But Pest offers a cleaner, function-based API that reads almost like plain English. Compare these two approaches to the same assertion:

// PHPUnit style
public function test_user_can_view_dashboard(): void
{
    $this->actingAs(User::factory()->create())
         ->get('/dashboard')
         ->assertStatus(200);
}

// Pest style
it('allows authenticated users to view the dashboard', function () {
    $user = User::factory()->create();

    actingAs($user)
        ->get('/dashboard')
        ->assertOk();
});

The Pest version is shorter, more expressive, and doesn’t require a class at all. For teams writing lots of tests, this compounds into a significant productivity gain. If you want to deepen your PHP and Laravel skills alongside Pest, Udemy has excellent Laravel courses that cover testing patterns in depth.

Installing Pest in a Laravel Project

Pest ships as a Composer package. If you’re starting fresh, new Laravel projects can include Pest during the installer prompt. For an existing project, run:

composer require pestphp/pest --dev --with-all-dependencies
composer require pestphp/pest-plugin-laravel --dev
./vendor/bin/pest --init

The --init command creates a Pest.php file in your tests/ directory. This is your global configuration file where you define shared setup, custom helpers, and dataset aliases.

Your tests/Pest.php will look something like this out of the box:

uses(Tests\TestCase::class)->in('Feature');
uses(Tests\TestCase::class)->in('Unit');

The uses() call binds Laravel’s base TestCase to every test in those directories, giving you full access to the application container, database helpers, and HTTP testing methods.

Writing Your First Feature Test

Feature tests in Laravel verify the behavior of your application from an HTTP perspective. Here’s a practical example testing user registration:

// tests/Feature/RegistrationTest.php

use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;

uses(RefreshDatabase::class);

it('registers a new user successfully', function () {
    $response = $this->post('/register', [
        'name'                  => 'Jane Doe',
        'email'                 => 'jane@example.com',
        'password'              => 'password',
        'password_confirmation' => 'password',
    ]);

    $response->assertRedirect('/dashboard');
    $this->assertDatabaseHas('users', ['email' => 'jane@example.com']);
});

it('fails registration with invalid email', function () {
    $response = $this->post('/register', [
        'name'                  => 'Jane Doe',
        'email'                 => 'not-an-email',
        'password'              => 'password',
        'password_confirmation' => 'password',
    ]);

    $response->assertSessionHasErrors('email');
});

Notice the uses(RefreshDatabase::class) at the top — this wraps each test in a transaction and rolls it back after, keeping your database clean without the overhead of a full migration per test.

Unit Tests and Expectations

Unit tests focus on individual classes and methods in isolation. Pest’s expectation API makes assertions feel natural:

// tests/Unit/OrderTest.php

use App\Models\Order;

it('calculates the correct total with tax', function () {
    $order = new Order(['subtotal' => 100, 'tax_rate' => 0.1]);

    expect($order->total())->toBe(110.0);
});

it('identifies orders as overdue after 30 days', function () {
    $order = new Order([
        'created_at' => now()->subDays(31),
        'status'     => 'pending',
    ]);

    expect($order->isOverdue())->toBeTrue();
});

The expect() API chains beautifully. You can call ->not->toBeNull(), ->toBeArray(), ->toHaveCount(3), and many more out of the box.

Using Datasets for Data-Driven Tests

One of my favorite Pest features is datasets. Instead of writing the same test five times with different inputs, you define the data separately:

it('validates email format', function (string $email, bool $isValid) {
    $validator = validator(['email' => $email], ['email' => 'email']);

    expect($validator->passes())->toBe($isValid);
})->with([
    ['valid@example.com', true],
    ['also.valid+tag@domain.co', true],
    ['missing-at-sign.com', false],
    ['@nodomain.com', false],
    ['spaces in@email.com', false],
]);

Pest runs this test once for each row, labeling each run in the output. This is massively useful for validation logic, edge cases, and boundary conditions.

Mocking and Faking in Pest

When you need to test code that interacts with external services, Laravel’s built-in fakes work seamlessly with Pest. Here’s testing a job dispatch:

use App\Jobs\SendWelcomeEmail;
use Illuminate\Support\Facades\Queue;

it('dispatches a welcome email job after registration', function () {
    Queue::fake();

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

    Queue::assertPushed(SendWelcomeEmail::class, function ($job) {
        return $job->email === 'john@example.com';
    });
});

The same pattern applies to Mail::fake(), Event::fake(), Notification::fake(), and Storage::fake(). No third-party mocking library needed.

Hooks and Shared Setup

Pest provides beforeEach() and afterEach() hooks for setup and teardown logic within a file, and beforeAll() / afterAll() for file-level setup:

// tests/Feature/PostTest.php

use App\Models\{Post, User};
use Illuminate\Foundation\Testing\RefreshDatabase;

uses(RefreshDatabase::class);

beforeEach(function () {
    $this->user = User::factory()->create();
    $this->actingAs($this->user);
});

it('creates a post', function () {
    $response = $this->post('/posts', [
        'title'   => 'Hello World',
        'content' => 'My first post content.',
    ]);

    $response->assertCreated();
    $this->assertDatabaseHas('posts', ['title' => 'Hello World']);
});

it('cannot create a post without a title', function () {
    $response = $this->post('/posts', ['content' => 'No title here.']);

    $response->assertUnprocessable();
});

The $this->user set in beforeEach is available in every test below it, removing the repetitive factory calls.

Running Tests and Coverage

Run your full suite with:

./vendor/bin/pest

Filter to a specific file or test name:

./vendor/bin/pest --filter="registers a new user"

Generate a code coverage report (requires Xdebug or PCOV):

./vendor/bin/pest --coverage --min=80

The --min=80 flag fails the run if coverage drops below 80%, which is handy for CI pipelines. If you’re deploying your tested Laravel app, Railway makes it straightforward to run Pest as part of a CI/CD pipeline before each deployment.

Pest Plugins Worth Knowing

  • pest-plugin-laravel — The essential plugin for Laravel-specific helpers like actingAs(), artisan(), and more.
  • pest-plugin-livewire — First-class Livewire component testing.
  • pest-plugin-faker — Access Faker directly in tests via the fake() helper.
  • pest-plugin-watch — Re-runs tests automatically on file change, great for TDD workflows.

Tips for Effective Laravel Testing with Pest

  1. Use RefreshDatabase for feature tests, not unit tests. Unit tests should never touch the database.
  2. Name tests like specifications. it('rejects expired coupons') reads as a product requirement, not just a test label.
  3. Keep tests focused. One assertion per test isn’t a hard rule, but each test should verify one behavior.
  4. Use factories liberally. Laravel’s model factories combined with Pest’s beforeEach hooks eliminate boilerplate fast.
  5. Run tests in parallel. Add --parallel to cut suite time significantly on larger projects.

Wrapping Up

Understanding how to test Laravel applications with Pest is one of the best investments you can make in code quality and developer confidence. The combination of Laravel’s built-in testing utilities and Pest’s expressive API removes most of the friction that makes developers skip writing tests in the first place.

Start with feature tests for your critical paths — authentication, payments, core business logic — and layer in unit tests as your codebase grows. Once you get comfortable with datasets and hooks, you’ll find yourself writing tests before the implementation, and that’s where the real productivity gains show up.

If you want a structured path through Laravel testing concepts and beyond, Udemy has courses that pair well with the hands-on practice of building real features with full test coverage.