Introduction
Pest is a testing framework built on top of PHPUnit that replaces the class-based, method-per-test style with top-level test() functions and a fluent expect() API. It ships pre-installed with every new Laravel app since Laravel 11, and the Laravel docs now use it as the default syntax in examples.
Key Concepts
test('description', closure): Declares a test. The closure is the test body.it('does something', closure): Alias fortest()that reads more like a sentence (BDD style).expect($value)->toBe(...): Fluent assertion API. Each matcher returns the expectation so you can chain.
Real World Context
Every test framework has friction. PHPUnit's is the ceremony — a namespace, a class, a method with test_ prefix, public function, : void, $this->. Pest strips all of that: a file with test(...) calls at the top level runs instantly. The reduced friction means people actually write the test instead of putting it off.
Deep Dive
Your first Pest test
php// tests/Feature/HomeTest.php <?php test('the home page returns a 200', function () { $response = $this->get('/'); expect($response->status())->toBe(200); }); it('shows the app name', function () { $this->get('/') ->assertSee(config('app.name')); });
No namespace, no class, no public function. Run it with php artisan test — Pest tests run alongside PHPUnit tests in the same suite.
The expect() API
phpexpect($value)->toBe(1); // strict equality expect($value)->toEqual(1); // loose equality expect($value)->toBeTrue(); expect($value)->toBeFalse(); expect($value)->toBeNull(); expect($value)->toBeInstanceOf(User::class); expect($array)->toHaveCount(3); expect($array)->toContain('laravel'); expect($string)->toStartWith('Hello'); expect($string)->toMatch('/^[a-z]+$/'); expect($collection)->each->toBeInstanceOf(Post::class); // Higher-order
The last form is Pest's killer feature: each->toBeInstanceOf(...) applies the assertion to every element in the collection, no foreach required.
Keeping Laravel helpers
Inside a Pest test closure, $this is bound to a TestCase instance, so you get every Laravel helper:
phpit('creates a post', function () { $user = User::factory()->create(); $response = $this->actingAs($user) ->post('/posts', ['title' => 'Hello']); $response->assertRedirect('/posts'); $this->assertDatabaseHas('posts', ['title' => 'Hello']); });
Converting a PHPUnit test
Before (PHPUnit):
phpclass UserTest extends TestCase { public function test_full_name(): void { $user = new User(['first_name' => 'Ada', 'last_name' => 'Lovelace']); $this->assertEquals('Ada Lovelace', $user->full_name); } }
After (Pest):
phptest('full name combines first and last', function () { $user = new User(['first_name' => 'Ada', 'last_name' => 'Lovelace']); expect($user->full_name)->toBe('Ada Lovelace'); });
Common Pitfalls
- Declaring
namespaceat the top of a Pest file — don't. Pest files are top-level scripts and a namespace breaks the autoloader discovery. - Using
$this->expects(...)thinking it's Mockery — Mockery's method isshouldReceive. Pest'sexpect()is unrelated and doesn't live on$this.
Best Practices
- Prefer
it()for behavioral tests — readingit creates a postas a sentence makes the intent obvious. - Use higher-order
each—expect($posts)->each->toBeInstanceOf(Post::class)is tighter than a loop.
Summary
- Pest replaces class-per-file with top-level
test()/it()functions. expect()is a fluent, chainable assertion API.- Inside the closure,
$thisis a LaravelTestCase— every helper still works. - PHPUnit and Pest tests can coexist in the same suite.