pestphp/pest
Pest is an elegant PHP testing framework focused on simplicity and developer joy. Write expressive, modern tests with a clean syntax, powerful expectations, and a great DX for PHP projects—built for fast feedback and readable suites.
Installation:
composer require pestphp/pest --dev --with-all-dependencies
Replace phpunit.xml with Pest’s stub (php artisan pest:init if using Laravel).
First Test:
Create tests/Feature/ExampleTest.php:
use Pest\Test;
test('the application returns a successful response', function () {
$response = $this->get('/');
$response->assertStatus(200);
});
Run Tests:
./vendor/bin/pest
tests/Pest.php for preset helpers (e.g., actingAs(), assertDatabaseHas()).toBeCasedCorrectly(), toUseTrait()).Unit Testing a Service Class:
use App\Services\UserService;
use Pest\Test;
test('UserService creates a user', function () {
$service = new UserService();
$user = $service->create(['name' => 'John', 'email' => 'john@example.com']);
expect($user)->toBeInstanceOf(\App\Models\User::class);
});
Test Organization:
describe() for grouping related tests:
describe('User Authentication', function () {
test('fails with invalid credentials')->expectException(\InvalidArgumentException::class);
test('succeeds with valid credentials')->expect(...);
});
it(): For edge cases within a describe block.Data-Driven Testing:
with() for parameterized tests:
test('math operations', function (int $a, int $b, int $expected) {
expect($a + $b)->toEqual($expected);
})->with([
[1, 2, 3],
[5, -3, 2],
]);
beforeEach:
beforeEach(function (array $data) {
$this->user = User::factory()->create($data);
});
test('user attributes', function (string $name, string $email) {
expect($this->user->name)->toBe($name);
})->with([
['John', 'john@example.com'],
['Jane', 'jane@example.com'],
]);
Laravel Integration:
test('GET /dashboard redirects guests', function () {
$response = $this->get('/dashboard');
$response->assertRedirect('/login');
});
assertDatabaseHas() or assertDatabaseMissing():
test('user is created in the database', function () {
$this->post('/register', ['name' => 'Test', 'email' => 'test@example.com']);
$this->assertDatabaseHas('users', ['email' => 'test@example.com']);
});
Browser Testing (Pest v4+):
test('login form submits correctly', function () {
$this->browse()
->visit('/login')
->fill('email', 'user@example.com')
->fill('password', 'password')
->press('Login')
->assertPathIs('/dashboard');
});
assertScreenshot() for pixel-perfect checks.Architecture Testing:
test('App\Services\PaymentService does not use App\Models\User directly')
->expect($this->class(App\Services\PaymentService::class))
->not->toUse(App\Models\User::class);
Parallel Testing: Speed up CI with --parallel:
./vendor/bin/pest --parallel --workers=4
./vendor/bin/pest --update-shards # Generate shards.json
./vendor/bin/pest --shard=1/5 # Run shard 1 of 5
CI/CD Optimization:
./vendor/bin/pest tests/Feature/AuthTest.php
./vendor/bin/pest --coverage --only-covered
Debugging:
dd(): Dump variables in tests (works in parallel):
test('debug user data', function () {
$user = User::find(1);
dd($user->toArray()); // Works even in parallel
});
Flaky Tests: Mark and retry:
test('flaky test example')->flaky();
Run with:
./vendor/bin/pest --flaky
Parallel Testing Quirks:
dd() Output: May appear out of order in parallel runs (fixed in v4.7.3). Use --no-parallel for debugging.beforeEach for setup.Laravel Preset Gaps:
actingAs() or assertDatabaseHas() are unavailable, ensure:
use Pest\Laravel\LaravelTestCase; is imported.Pest.php preset is properly configured (check tests/Pest.php).App\Http) may be excluded by default. Adjust rules in tests/ArchitectureTest.php.Dataset Issues:
with() is called on the correct scope (e.g., describe()->with() vs. test()->with()).fn() for complex data:
test('dynamic data', function (array $data) {
// ...
})->with(fn () => [
['key' => 'value1'],
['key' => 'value2'],
]);
Browser Testing:
--headed for debugging:
./vendor/bin/pest --headed
waitFor():
$this->browse()->waitFor(1000)->assertSomething();
CI-Specific Bugs:
variables:
CI_SERVER_URL: "https://gitlab.example.com"
--teamcity is used with --parallel.Test Isolation:
test('without rollback', function () {
$this->withoutExceptionHandling();
// ...
})->uses('App\Tests\NoRollbackTestCase');
Slow Tests:
--profile to identify bottlenecks:
./vendor/bin/pest --profile
shards.json and commit it to the repo for consistent CI performance.Assertion Failures:
expect($actual)->toBe($expected)->dump() for detailed diffs.tests/Extensions.php:
use Pest\Extension;
Extension::macro('toBeEven', function (int $value) {
return expect($value)->toBeEven();
});
Custom Assertions:
tests/Extensions.php:
use Pest\Extension;
Extension::macro('toBeJsonApi', function (array $expected) {
return expect(json_encode($this))->toBeJsonApi($expected);
});
Test Templates:
pest:init to scaffold custom templates (e.g., for API tests):
php artisan pest:init --template=api
Plugins:
How can I help you explore Laravel packages today?