Tilly The Coder
Tilly The Coder
What's New in Pest v4 for Laravel 12: A Practical Developer-First Guide
Tilly The Coder

What's New in Pest v4 for Laravel 12: A Practical Developer-First Guide

Tilly The Coder
5 mins
This is the estimated time it takes to read the article.

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.

Browser testing is the headline

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 install

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

Use browser tests where they buy confidence

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

Architecture tests keep the app honest

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']);

Mutation testing tells you whether tests assert behavior

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 --parallel

Use 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=80

Snapshot and type coverage still matter

Snapshots 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();
});

A Laravel 12 testing stack I would ship

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