Pest v4.0.0 was published on Aug 21, 2025. By the Apr 14, 2026 crawl date, Pest 4.6.0 had also shipped, so the practical focus is Pest 4 as it was being used in real Laravel 12 projects: browser tests, architecture checks, mutation testing, snapshots, and type coverage in one testing style.
The big idea is simple: keep the fast HTTP and unit tests you already trust, then add browser coverage only where the browser is actually the thing you need to verify.
Pest v4 browser testing is built around Playwright. You can write tests with Pest's fluent syntax and still use Laravel testing tools such as factories, events, authentication assertions, and database refreshes.
composer require pestphp/pest-plugin-browser --dev
npm install playwright@latest
npx playwright installAdd tests/Browser/Screenshots to .gitignore before the first real run. Screenshots are useful artifacts when debugging locally or in CI, but they should not become noisy repository files.
it('lets a user sign in', function (): void {
User::factory()->create([
'email' => 'tilly@example.com',
'password' => bcrypt('password'),
]);
$page = visit('/')->on()->mobile();
$page->click('Login')
->assertUrlIs('/login')
->fill('email', 'tilly@example.com')
->fill('password', 'password')
->click('Submit')
->assertSee('Dashboard');
$this->assertAuthenticated();
});The browser API can target different browsers and devices. Chrome is the default, but Pest can run Firefox or Safari through command options or configuration. It can also run mobile profiles through on()->mobile() or specific devices such as iPhone14Pro. That makes it practical to cover one desktop flow and one mobile flow without writing two completely separate test suites.
$page = visit('/pricing')
->on()->iPhone14Pro()
->inDarkMode()
->withLocale('en-GB')
->withTimezone('Europe/London');
$page->assertSee('Yearly')
->click('@subscribe-yearly')
->assertSee('Checkout');Useful browser options include headed debug mode, parallel execution, custom user agents, host configuration for subdomains, geolocation presets, timezone, locale, dark mode, screenshots, console log assertions, JavaScript error assertions, and smoke checks across multiple pages. Use them where they match the product risk.
Sign in, register, reset password, and subscribe flows.
Multi-step forms and modals.
Livewire or Alpine interactions where DOM state matters.
Critical responsive layouts.
File uploads, previews, and payment flows where the UI matters.
Avoid browser-testing every validation rule. A Laravel HTTP test can usually cover those in a fraction of the time and with less setup.
Pest architecture testing is a good fit for Laravel codebases that grow quickly. You can enforce simple boundaries without writing a custom static analysis tool.
arch('controllers do not use models directly')
->expect('App\Http\Controllers')
->not->toUse('Illuminate\Database\Eloquent\Model');Architecture tests are strongest when the rules are local and obvious. For example: actions must be final, data objects must be readonly, controllers must stay thin, requests must not be used outside HTTP, and domain code must not call response() directly. Weak architecture tests usually try to encode the whole company style guide at once.
arch('actions are final')
->expect('App\Actions')
->classes()
->toBeFinal();
arch('app avoids debugging helpers')
->expect('App')
->not->toUse(['dd', 'dump', 'ray']);Code coverage says a line ran. Mutation testing asks a better question: if the code changed in a small way, would the test fail?
./vendor/bin/pest --mutate --parallelUse this on important actions, policies, value objects, and billing or permission logic. It is more expensive than normal tests, so run it intentionally rather than on every local save.
Pest mutation testing can be scoped with covers() or mutates(), then tuned with options such as --id, --everything, --covered-only, --bail, --class, --ignore, --clear-cache, --no-cache, --profile, --retry, --stop-on-uncovered, --stop-on-untested, and --min. The best first run is narrow. Pick the code that would hurt if it lied, then widen after the team trusts the signal.
covers(ApplyCoupon::class);
it('rejects expired coupons', function (): void {
$coupon = Coupon::factory()->expired()->create();
expect(app(ApplyCoupon::class)->handle($coupon))->toBeFalse();
});./vendor/bin/pest --mutate --parallel --covered-only --min=80Snapshots are useful when output shape matters and hand-writing every assertion would obscure intent. API resources, generated configuration, emails, and rendered rich content are good candidates.
Use snapshots for stable output, not for hiding uncertainty. If a Blade component or JSON resource changes every week, a snapshot may become a click-to-accept habit. If a public API response or transactional email must not drift, a snapshot can be the cleanest test.
it('renders the invoice email', function (): void {
$invoice = Invoice::factory()->paid()->create();
expect(new InvoicePaidMail($invoice))->toMatchSnapshot();
});Fast feature tests for routes, policies, validation, billing decisions, and API contracts.
Unit tests for actions, value objects, parsers, and price calculations.
Browser tests for the handful of flows where DOM state, JavaScript, or responsiveness matters.
Architecture tests for project boundaries that should stay true for years.
Mutation tests on high-risk code, run intentionally in CI or before release.
Snapshots only for stable output that is painful to assert by hand.
The Pest v4 mindset is not browser test everything. It is choose the test level that catches the bug with the least ceremony, then make that test easy enough that the team keeps running it.