Skip to main content

Why did Pest become Laravel’s default?

Starting with Laravel 11, Pest is selected as the default test framework when you create a project with laravel new. PHPUnit has long been the standard testing tool for PHP, but Pest builds on top of PHPUnit while offering a more concise, more readable syntax.
Because Pest runs on top of PHPUnit, your existing PHPUnit tests continue to work. You can migrate incrementally.
Behind the official adoption of Pest is the goal of reducing friction while writing tests. By dropping the ritual of defining a class and writing methods and letting you focus on “what you want to test,” Pest helps make writing tests a natural habit.

Main differences from PHPUnit

Test syntax

The most immediately visible difference is how tests are written.
test('a user can log in', function () {
    $user = User::factory()->create();

    $response = $this->post('/login', [
        'email' => $user->email,
        'password' => 'password',
    ]);

    $response->assertRedirect('/dashboard');
});
Pest’s test() function takes a closure. Because there’s no class or method definition, the test’s intent is clear from the first line. it() works the same way. Written in English, it('can login', ...) reads as a natural sentence.

The expect() API

Pest’s headline feature is chained assertions via expect().
test('the posts API returns JSON', function () {
    $posts = Post::factory(3)->create();

    $response = $this->getJson('/api/posts');

    expect($response->status())->toBe(200);
    expect($response->json('data'))->toHaveCount(3);
    expect($response->json('data.0.title'))->not->toBeEmpty();
});
Assertions like expect($value)->toBe(), ->toBeNull(), ->toContain(), and ->toHaveCount() read naturally in English. You can also combine multiple assertions with method chaining.
expect($user)
    ->name->toBe('Taro Tanaka')
    ->email->toBe('[email protected]')
    ->is_admin->toBeFalse();

Setup and teardown

beforeEach() corresponds to PHPUnit’s setUp(); afterEach() corresponds to tearDown().
beforeEach(function () {
    $this->user = User::factory()->create();
    $this->actingAs($this->user);
});

test('authenticated users can access their profile', function () {
    $response = $this->get('/profile');
    $response->assertOk();
});

test('authenticated users can update their profile', function () {
    $response = $this->patch('/profile', ['name' => 'A new name']);
    $response->assertRedirect('/profile');
});

Datasets — table-driven tests

Use dataset to run the same test logic against multiple sets of input.
test('invalid email addresses fail validation', function (string $email) {
    $response = $this->postJson('/api/users', [
        'name' => 'Taro Tanaka',
        'email' => $email,
    ]);

    $response->assertUnprocessable();
})->with([
    'no email' => [''],
    'malformed' => ['notanemail'],
    'double @' => ['a@@example.com'],
]);
The label of each dataset (e.g. 'no email') is appended to the test name, so it’s obvious at a glance which pattern failed.

arch() tests — automated architecture checks

Pest’s arch() tests verify the structure of your codebase. You can automatically check architecture rules like “controllers must not depend directly on models” or “models must extend Eloquent.”
test('models extend Eloquent', function () {
    arch()->expect('App\Models')
        ->toExtend(Illuminate\Database\Eloquent\Model::class);
});

test('controllers are final', function () {
    arch()->expect('App\Http\Controllers')
        ->toBeFinal();
});

test('service classes do not depend on controllers', function () {
    arch()->expect('App\Services')
        ->not->toUse('App\Http\Controllers');
});
arch() tests run static analysis on the source code. Because they don’t send HTTP requests or hit the database, they’re extremely fast.
Pest also ships presets of common architecture rules.
test('follows Laravel architecture rules', function () {
    arch()->preset()->laravel();
});
The laravel() preset verifies model naming, controller inheritance, middleware structure, and other common Laravel conventions in one go.

Practical example in a Laravel project

Creating tests

php artisan make:test UserTest
With the default settings on Laravel 11+, this generates a Pest-style test file.
<?php

test('example', function () {
    expect(true)->toBeTrue();
});

Using Laravel test helpers as-is

Because Pest extends Laravel’s TestCase, all Laravel test helpers—actingAs(), assertDatabaseHas(), HTTP test helpers, etc.—work as usual.
use App\Models\Post;
use App\Models\User;

test('users can delete their own posts', function () {
    $user = User::factory()->create();
    $post = Post::factory()->for($user)->create();

    $response = $this
        ->actingAs($user)
        ->delete("/posts/{$post->id}");

    $response->assertRedirect('/posts');
    $this->assertModelMissing($post);
});

test('users cannot delete other users\' posts', function () {
    $user = User::factory()->create();
    $otherUser = User::factory()->create();
    $post = Post::factory()->for($otherUser)->create();

    $this
        ->actingAs($user)
        ->delete("/posts/{$post->id}")
        ->assertForbidden();

    $this->assertModelExists($post);
});

Factories and database transactions

Traits like RefreshDatabase and DatabaseTransactions can also be applied easily with uses(). Writing this at the top of a file applies it to the whole file.
uses(Tests\TestCase::class, Illuminate\Foundation\Testing\RefreshDatabase::class)->in('Feature');
Setting this once in tests/Pest.php automatically enables RefreshDatabase for all Feature tests. You can also override it in individual test files.

Coexisting with existing PHPUnit tests

Pest and PHPUnit can coexist in the same project. You don’t have to rewrite existing PHPUnit tests—just start writing new tests in Pest style.
./vendor/bin/pest       # Run all tests with Pest (including PHPUnit tests)
./vendor/bin/pest --filter="user"  # Filter by test name
php artisan test        # Pest works via the Artisan command too
Since Laravel 11, if Pest is installed, php artisan test automatically runs via Pest.

How to approach migration

  1. First add uses() configuration to tests/Pest.php
  2. Write new tests in Pest style
  3. Migrate existing PHPUnit tests gradually while verifying behavior
You don’t need to rewrite all tests immediately. Pest’s syntax has a lower learning curve than PHPUnit, so team adoption tends to be smooth.

Summary

ComparisonPHPUnitPest
Test definitionClass + methodtest() / it() function
Assertions$this->assert*()expect()->to*()
SetupsetUp()beforeEach()
Data-driven@dataProvider->with()
Architecture checksNonearch()
SpeedStandardEquivalent (runs on PHPUnit)
Pest isn’t a replacement for PHPUnit but a higher layer wrapping PHPUnit. Its Laravel integration is deep enough that it’s the default at starter kit generation time. The adoption cost for existing projects is low, and you can start reaping the benefits by writing just new tests in Pest. The more tests you write, the sooner bugs are caught and the lower the psychological barrier to refactoring. Pest is a tool for reducing “the friction of writing tests themselves.”

Pest official docs

See the official documentation for all Pest features, including datasets, coverage, and parallel execution.
Last modified on July 13, 2026