Getting started with Laravel testing using Pest PHP
An introduction to Pest PHP—the default from Laravel 11—from a senior engineer’s perspective. Covers differences from PHPUnit, migration costs, and using arch() tests, with real project adoption in mind.
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.
The most immediately visible difference is how tests are written.
Pest
PHPUnit
test('a user can log in', function () { $user = User::factory()->create(); $response = $this->post('/login', [ 'email' => $user->email, 'password' => 'password', ]); $response->assertRedirect('/dashboard');});
class AuthTest extends TestCase{ public function test_user_can_login(): void { $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.
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.
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');});
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.
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.
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 namephp artisan test # Pest works via the Artisan command too
Since Laravel 11, if Pest is installed, php artisan test automatically runs via Pest.
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.