Pest 5 Released With Test Impact Analysis, Agent Verification, and Evals
Last updated on by Paul Redmond
The next major release of Pest PHP, Pest v5, is here! Nuno Maduro announced Pest 5 on stage at Laracon US 2026 in Boston and tagged v5.0.0 during the conference. This release features the TIA engine, which drastically reduces test suite completion times. Pest 5 requires PHP 8.4 and PHPUnit 13, and it gathers up a set of first-party plugins for AI Agents, PHPStan, Rector, and more.
The TIA engine is a game-changer for test speeds. Laravel Cloud's test suite of 19,000+ tests went from 3 minutes to 5 seconds:
Here's what's new in Pest 5 at a glance:
- Tia Engine: re-runs only the tests your latest changes affected and replays the rest from cache, without giving up coverage accuracy
- Agent plugin: hands your AI coding agent a single command to check whether a change actually works, inside your real test suite
- Evals plugin: scores LLM output with deterministic checks and AI-powered scorers through the
expect()API you already use - PHPStan plugin: teaches PHPStan about
it(),test(),expect(), and the$thisinside your test closures - Rector plugin: 60 rules that rewrite raw PHP assertions as Pest matchers and handle major version upgrades
- New expectations for emails, ULIDs, IP addresses, and other formats that are tedious to check by hand
- PHP 8.4 and PHPUnit 13 as the new baseline
What's New
Test Impact Analysis With the Tia Engine
Tia is short for Test Impact Analysis. The first run records which tests touch which files, and every run after that executes only the tests affected by what you changed, replaying cached results for the rest.
Add --tia to any invocation:
./vendor/bin/pest --parallel --tia
The summary tells you how the run was resolved:
Tests: 774 passed (2658 assertions, 7 affected, 2 uncached, 765 replayed)Duration: 3.92s
A replay isn't a skipped test, either. Each cached result stores everything the test produced, down to the exact lines and branches it covered, so --coverage reports and --min thresholds behave as though the whole suite ran.
What makes this interesting is that the dependency graph understands more than PHP files. Change a migration and Pest re-runs only the tests that queried that table. Edit a shared JS component and it walks Vite's module graph to find the Inertia pages importing it. Touch a Blade template and it re-runs the tests that rendered it. It detects Laravel, Symfony, Livewire, Inertia, and browser assets through Composer, so there's nothing to configure.
Pest also normalizes files before hashing them, stripping whitespace, comments, and docblocks. A Pint pass or a README edit produces an identical hash and runs nothing at all.
Note: recording the baseline needs a coverage driver, so you'll want PCOV or Xdebug installed. On a large suite that first run takes a while, which is why teams can have CI record the baseline once per merge to main and let everyone else download it.
You can configure the behavior in tests/Pest.php:
pest()->tia() ->always() // run TIA on every invocation ->locally() // restrict always() to local environments only ->baselined() // fetch shared baseline from CI ->filtered(); // load only affected test files
Verifying AI Agent Changes With the Agent Plugin
Coding agents write code readily enough, but they have no good way to confirm it works. The Agent plugin gives them one. Install it as a dev dependency:
composer require pestphp/pest-plugin-agent --dev
It adds an --agent option that runs a snippet inside a full Pest test, with your factories, RefreshDatabase, and Laravel fakes available exactly as they'd be in a feature test:
./vendor/bin/pest --agent='$user = \App\Models\User::factory()->create(); $this->actingAs($user)->get("/dashboard")->assertOk();'
Use single quotes so your shell doesn't interpolate $user, and fully qualify your class names. Each snippet runs as an isolated test named "verify," and you can pass --agent more than once.
If you have the browser testing plugin installed, the same probe can drive a real browser and assert the backend side effects it set off, which a UI-only check can't see:
./vendor/bin/pest --agent='\Illuminate\Support\Facades\Mail::fake(); visit("/contact")->type("email", "test@example.com")->press("Send")->assertSee("Message sent");'
Pest is clear that this is meant for quick feedback while you work, not a replacement for the regression tests you commit.
Testing LLM Output With Evals
Send the same prompt to a model twice and you'll get two different answers, which makes equality assertions close to useless. The Evals plugin scores output quality instead, using the same expect() API:
composer require pestphp/pest-plugin-evals --dev
use App\Agents\CapitalCityAgent; it('answers capital city questions correctly', function (): void { expect(CapitalCityAgent::class) ->prompt('What is the capital of France?') ->toContain('Paris') // deterministic check ->toBeRelevant() // LLM-as-judge scorer ->toBeSimilar('Paris, France'); // semantic similarity});
Every eval calls a real model, so they're skipped on a normal run and cost you nothing. Pass --evals when you want them:
./vendor/bin/pest # evals skipped, no API calls./vendor/bin/pest --evals # real model, all scorers active
The deterministic expectations (toContain(), toMatch(), toBe(), toBeJson()) need no driver at all. The scored ones each take a threshold between 0.0 and 1.0 that defaults to 0.7. You get toBeRelevant() for grading a response against its prompt, toBeSafe() for unsafe content and prompt injection resistance, toBeFactual() for checking against a reference answer, toBeSimilar() for embeddings-based similarity, and toPassJudge() for criteria you write out in plain English. There's also toHaveToolCalls() and toFollowTrajectory() to confirm an agent reached for the right tools in the right order, repeat() for sampling a prompt several times, and toPassScorer() if you'd rather write your own.
Scoring runs through Laravel AI by default. Both the judge and embeddings drivers accept closures or custom classes, so another provider is a few lines in tests/Pest.php.
PHPStan Support for Pest Tests
First-party PHPStan support has been one of the most requested features from the community for a while. Out of the box PHPStan has no idea what it(), test(), or expect() are, and no idea what $this refers to inside a test closure:
composer require pestphp/pest-plugin-phpstan --devcomposer require phpstan/phpstan --dev
The plugin reads your Pest.php configuration to work out what $this should be, and supports both the uses(TestCase::class)->in(...) and pest()->extend(...)->use(...)->in(...) styles. If you use phpstan/extension-installer it registers itself; otherwise add it to phpstan.neon:
includes: - vendor/pestphp/pest-plugin-phpstan/extension.neon
Types flow through expectation chains from there, so toBeInt() narrows an int|string, properties you assign in beforeEach() are typed on $this, and higher-order expectations like expect($user)->name->toBe('Nuno') resolve from the underlying value. It also catches expectations that can never pass:
expect(10)->toStartWith('1'); // int can never satisfy toStartWith()
On top of the type inference you get Pest-aware rules for static test closures, $this inside beforeAll(), duplicate test descriptions, and invalid throws() and covers() references. Each rule has a stable identifier such as pest.expectation.impossible, so you can ignore them selectively.
Refactoring Tests With Rector
The Rector plugin ships 60 rules split across coding style and version upgrade sets:
composer require pestphp/pest-plugin-rector --devcomposer require rector/rector --dev
Add a set to your rector.php:
use Pest\Rector\Set\PestSetList;use Rector\Config\RectorConfig; return RectorConfig::configure() ->withPaths([__DIR__ . '/tests']) ->withSets([ PestSetList::CODING_STYLE, ]);
The coding style set rewrites raw PHP assertions as matchers and chains redundant expectations together:
-expect(count($array))->toBe(5);-expect(array_key_exists('id', $array))->toBeTrue();+expect($array)->toHaveCount(5)+ ->toHaveKey('id');
Other rules convert PHPUnit assertions to expect(), turn expect($value > 10)->toBeTrue() into toBeGreaterThan(10), collapse indexed array assertions into sequence(), and swap try/catch exception tests for toThrow(). The version sets under PestLevelSetList handle major upgrades and are cumulative. Preview everything with vendor/bin/rector process --dry-run before you let it loose.
Time-Balanced Sharding
Time-balanced sharding isn't new in 5.0, but it's worth repeating if you missed it, because it landed quietly in Pest v4.6.0 back in April. Splitting a suite across CI machines by file count tends to leave one shard grinding away long after the others have gone home. Pest distributes by recorded runtime instead.
Record the timings once:
./vendor/bin/pest --update-shards
Then commit tests/.pest/shards.json, and --shard picks it up on its own:
./vendor/bin/pest --shard=1/4
Add test files before you refresh the timings and your suite still runs. New files get distributed evenly while the known ones stay time-balanced, and Pest warns you that the file is stale.
New Expectations for Emails, ULIDs, and IP Addresses
Eight new matchers cover the format checks that are a nuisance to write by hand:
expect('nuno@pestphp.com')->toBeEmail();expect('01ARZ3NDEKTSV4RRFFQ69G5FAV')->toBeUlid();expect('192.168.1.1')->toBeIpAddress();expect('00:1a:2b:3c:4d:5e')->toBeMacAddress();expect('example.com')->toBeHostname();expect('example.co.uk')->toBeDomain();expect('Zm9vYmFy')->toBeBase64();expect('deadbeef')->toBeHexadecimal();
Each one takes an optional custom failure message, and you can negate any of them with not.
Upgrading to Pest 5
Pest 5 requires PHP 8.4 or greater and runs on PHPUnit 13, and that second one is where most of your upgrade friction will come from rather than Pest itself. Give the PHPUnit 13 changelog a read for anything that affects your suite. Pest's own upgrade guide puts the job at about two minutes and documents no API-level breaking changes beyond the version bumps.
For most suites it's one line in composer.json:
- "pestphp/pest": "^4.0",+ "pestphp/pest": "^5.0",
Move any Pest-maintained plugins to ^5.0 while you're in there. Two things worth knowing before you reach for the headline feature: the Tia Engine needs PCOV or Xdebug to record its baseline, and CI baseline sharing requires an authenticated GitHub CLI and only works on GitHub.
References
You can read the full Pest 5 announcement and the upgrade guide on the Pest website, along with the documentation for the Tia Engine, Agent, Evals, PHPStan, and Rector plugins.
Pest is created and maintained by Nuno Maduro, and you can find the v5.0.0 release, the full diff, and the source code on GitHub at pestphp/pest.