Pest 5 Released With Test Impact Analysis, Agent Verification, and Evals

Last updated on by

Pest 5 Released With Test Impact Analysis, Agent Verification, and Evals image

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:

Taylor Otwell on X: Pest 5 took our Laravel Cloud test suite (19,000+ tests) 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 $this inside 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 --dev
composer 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 --dev
composer 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.

Paul Redmond photo

Staff writer at Laravel News. Full stack web developer and author.

Filed in:
Cube

Laravel Newsletter

Join 40k+ other developers and never miss out on new tips, tutorials, and more.

image
Laravel Cloud

Easily create and manage your servers and deploy your Laravel applications in seconds.

Visit Laravel Cloud
Shift logo

Shift

Running an old Laravel version? Instant, automated Laravel upgrades and code modernization to keep your applications fresh.

Shift
Harpoon: Next generation time tracking and invoicing logo

Harpoon: Next generation time tracking and invoicing

The next generation time-tracking and billing software that helps your agency plan and forecast a profitable future.

Harpoon: Next generation time tracking and invoicing
Laravel Cloud logo

Laravel Cloud

Easily create and manage your servers and deploy your Laravel applications in seconds.

Laravel Cloud
No Compromises logo

No Compromises

Joel and Aaron, the two seasoned devs from the No Compromises podcast, are now available to hire for your Laravel project. ⬧ Flat rate of $9500/mo. ⬧ No lengthy sales process. ⬧ No contracts. ⬧ 100% money back guarantee.

No Compromises
SaaSykit: Laravel SaaS Starter Kit logo

SaaSykit: Laravel SaaS Starter Kit

SaaSykit is a Multi-tenant Laravel SaaS Starter Kit that comes with all features required to run a modern SaaS. Payments, Beautiful Checkout, Admin Panel, User dashboard, Auth, Ready Components, Stats, Blog, Docs and more.

SaaSykit: Laravel SaaS Starter Kit
Kirschbaum logo

Kirschbaum

Providing innovation and stability to ensure your web application succeeds.

Kirschbaum
Lucky Media logo

Lucky Media

Get Lucky Now - the ideal choice for Laravel Development, with over a decade of experience!

Lucky Media
PhpStorm logo

PhpStorm

The go-to PHP IDE with extensive out-of-the-box support for Laravel and its ecosystem.

PhpStorm
Acquaint Softtech logo

Acquaint Softtech

Acquaint Softtech offers AI-ready Laravel developers who onboard in 48 hours at $3000/Month with no lengthy sales process and a 100 percent money-back guarantee.

Acquaint Softtech

The latest

View all →
Queue-SQL: Run Mass Deletes and Updates Across Parallel Queue Jobs image

Queue-SQL: Run Mass Deletes and Updates Across Parallel Queue Jobs

Read article
Blade Formatting in Laravel Pint image

Blade Formatting in Laravel Pint

Read article
Inertia DevTools Is Now on the Chrome Web Store image

Inertia DevTools Is Now on the Chrome Web Store

Read article
Laravel LSP: A First-Party Language Server Announced at Laracon US 2026 image

Laravel LSP: A First-Party Language Server Announced at Laracon US 2026

Read article
Watch Laracon US 2026 Live on YouTube image

Watch Laracon US 2026 Live on YouTube

Read article
Monthly Log Driver in Laravel 13.23 image

Monthly Log Driver in Laravel 13.23

Read article