Pest PHP: from zero to productive in an afternoon
Why I swapped PHPUnit for Pest across all my projects, and how to build a suite that actually survives over time.
For years I wrote tests with PHPUnit and thought it was fine. Then I tried Pest on a small project and never went back. It's not that PHPUnit is bad: it's that Pest strips away the noise between you and the intent of the test.
The difference on one screen
This is PHPUnit:
class PostTest extends TestCase
{
public function test_a_guest_cannot_create_a_post(): void
{
$response = $this->post('/posts', ['title' => 'Hello']);
$response->assertRedirect('/login');
}
}And this is exactly the same thing in Pest:
it('does not let a guest create a post', function () {
$this->post('/posts', ['title' => 'Hello'])
->assertRedirect('/login');
});It reads like a sentence. Once your suite has three hundred tests, that difference stops being aesthetic and becomes economic.
Datasets: the secret weapon
This is where Pest really pulls ahead. Datasets turn five copy-pasted tests into a single one:
it('rejects invalid emails', function (string $email) {
expect(fn () => User::create(['email' => $email]))
->toThrow(ValidationException::class);
})->with([
'no at sign' => 'ismaelexample.com',
'no domain' => 'isma@',
'empty' => '',
'with spaces' => 'isma @example.com',
]);Each case is reported separately with its own name, so when one fails you know exactly which without reading a single line of code.
Higher order tests
For trivial checks you can chain directly:
it('has a home page')
->get('/')
->assertOk();Don't overuse this. It works beautifully for smoke tests and becomes unreadable the moment there are three assertions.
How I set up the suite
My tests/Pest.php almost always ends up like this:
uses(Tests\TestCase::class, Illuminate\Foundation\Testing\RefreshDatabase::class)
->in('Feature');
expect()->extend('toBeSlug', function () {
return $this->toMatch('/^[a-z0-9]+(?:-[a-z0-9]+)*$/');
});Two ideas behind that:
RefreshDatabaseonly inFeature. Unit tests shouldn't touch the database; if one needs it, that's usually a sign the logic lives in the wrong place.- Domain-specific expectations.
toBeSlug,toBeValidInvoice,toBeWithinBusinessHours. The test ends up speaking the language of the business instead of the language of the framework.
Worth it
Migrating from PHPUnit is incremental: Pest runs your PHPUnit tests untouched. You can install it today, write new tests in Pest and never migrate the old ones if you don't feel like it.
That's probably the best reason to give it a try.