Skip to content

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/pest

Getting started

The smallest useful thing you can do with it, and what each part means.

Tests without classes
<?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);
`each` applies the following expectation to every element, which replaces a foreach loop of assertions and reports which element failed.

Advanced usage

Where the library earns its place over a simpler alternative.

Hooks, architecture tests and coverage
<?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=80
Architecture tests are Pest's distinctive feature: they encode structural rules — layering, finality, forbidden functions — that a linter cannot express and code review otherwise has to enforce by hand.

Errors 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.