What it is
Pest is a testing framework built on PHPUnit with a concise closure-based syntax, expectations API and built-in parallel execution.
Tests are closures passed to test() or it(). The expect() API chains readable assertions, and datasets replace data providers.
Installation
composer require --dev pestphp/pestGetting started
The smallest useful thing you can do with it, and what each part means.
php
<?php
it('slugifies a title', function () {
expect(slugify('Hello World'))->toBe('hello-world');
});
it('rejects an empty title', function () {
expect(fn () => new Book(''))
->toThrow(InvalidArgumentException::class, 'title required');
});
// Datasets replace data providers.
it('slugifies correctly', function (string $input, string $expected) {
expect(slugify($input))->toBe($expected);
})->with([
['Hello World', 'hello-world'],
['C++ & Go!', 'c-go'],
]);
// Chained expectations
expect($books)
->toHaveCount(3)
->each->toBeInstanceOf(Book::class)
->and($books[0]->year)->toBeGreaterThan(1900);Advanced usage
Where the library earns its place over a simpler alternative.
php
<?php
beforeEach(function () {
$this->repository = new InMemoryBookRepository();
});
// Architecture tests — enforce structure, not behaviour.
arch('controllers are final')
->expect('App\Http\Controllers')
->toBeFinal()
->toExtend('App\Http\Controllers\Controller');
arch('domain does not depend on the framework')
->expect('App\Domain')
->not->toUse(['Illuminate', 'Symfony']);
arch('no debug statements')
->expect(['dd', 'dump', 'var_dump', 'ray'])
->not->toBeUsed();
// vendor/bin/pest --parallel --coverage --min=80Errors and fixes
The failures you are most likely to hit, and what actually resolves them.
- Call to a member function on null inside a test
- $this properties are set in beforeEach. A closure defined outside a test binding will not have them.
- Parallel tests interfere
- Shared state such as a database or files. Use per-process isolation, or mark those tests to run sequentially.
Best practices
- Use arch() tests to enforce layering and catch stray debug calls automatically.
- Run with --parallel; test suites of any size benefit substantially.
- Adopt incrementally — existing PHPUnit tests keep working alongside Pest ones.
- Set --min coverage in CI so coverage cannot silently decline.
Background
Why it exists, and what it was reacting to.
Created by Nuno Maduro, Pest keeps PHPUnit's engine — so every PHPUnit test still works — while replacing the class-per-test-file ceremony with plain functions.
