tillythecoder.com
tillythecoder.com
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 · ·
8 mins
This is the estimated time it takes to read the article.
PHP
Laravel

If you’ve been waiting for browser testing that actually feels Laravel-native, Pest v4 is the release. It lands with first-class browser tests (powered by Playwright), visual and smoke testing, device + dark-mode emulation, sharding for blazing-fast CI, and a bunch of quality-of-life wins—while staying true to Pest’s minimal, readable syntax. And yes: it’s ready for modern Laravel 12 projects.

This article walks you through what’s new, why it matters for Laravel 12 apps, and how to adopt it safely in existing codebases.


TL;DR (for the busy dev)

  • Browser Testing: Write end-to-end tests in Pest with Laravel’s testing helpers, backed by Playwright. Works with Notification::fake(), factories, RefreshDatabase, etc.

  • Smoke Testing: One-liner to visit many routes and fail on JS errors or console logs.

  • Visual Regression: Snapshot UI with assertScreenshotsMatches() to catch pixel-level regressions.

  • Devices & Themes: Emulate iPhone/MacBook/mobile/tablet and flip dark/light mode in one chain.

  • Parallel + Sharding: Scale horizontally in CI (--parallel --shard=1/4) for significant speed-ups.

  • Faster Type Coverage: New engine is ~2× faster on first run and instant thereafter.

  • Extra niceties: Profanity checker, skipOnCi()/skipLocally(), and PHPUnit 12 under the hood.

  • Requirements: PHP 8.3+, with the Laravel plugin supporting Laravel 12.x.


1) Browser testing that feels like unit tests

The headline feature: first-class browser testing, powered by Playwright. The API is intentionally Pest-simple—visit(), then chain interactions and assertions. Crucially, it integrates with Laravel’s testing tools: you can actingAs() a user, fake events/notifications, use the in-memory database via RefreshDatabase, and call familiar assertions like assertAuthenticated().

it('lets a user reset their password', function () {
    Notification::fake();
    $this->useDatabase(); // via RefreshDatabase on Pest.php

    $user = \App\Models\User::factory()->create();

    $page = visit('/sign-in')
        ->on()->mobile()      // emulate a mobile device
        ->inDarkMode();       // test dark mode too

    $page->click('Forgot Password?')
         ->type('email', $user->email)
         ->press('Send Reset Link')
         ->assertSee('We have emailed your password reset link!')
         ->assertNoJavascriptErrors();   // keep the console clean

    Notification::assertSentTo($user, \App\Notifications\ResetPassword::class);
});

Beyond clicks and typing, you can drag-and-drop, attach files, script the page, read HTML content, take screenshots, run headed mode, and even pause and tinker during the test for fast feedback loops.

Getting started (once):
composer require pestphp/pest-plugin-browser --dev
npm install playwright@latest && npx playwright install
(and add tests/Browser/Screenshots to .gitignore).

2) Device testing & dark/light mode in one chain

Need to verify responsive behavior or color schemes? Pest v4 bakes it right into the fluent API:

$page = visit('/pricing')
    ->on()->iPhone14Pro()     // or ->mobile(), ->tablet(), ->macbook14()
    ->inLightMode();       // or ->inDarkMode()

This reads exactly like the intent: “visit pricing, pretend to be an iPhone 15, force light mode” - no extra harness code required. It’s a small thing that compounds into less flakiness and more readable tests.

3) Smoke testing: banish silent client-side failures

A lot of “it works on my machine” moments come from unnoticed JS errors or noisy console logs. Pest v4 gives you a one-liner to scan multiple routes and fail on those issues:

$routes = ['/', '/docs', '/pricing', '/contact'];

visit($routes)->assertNoSmoke(); 

You can also call assertNoJavaScriptErrors() or assertNoConsoleLogs() explicitly. This is perfect for PR gates and nightly CI to catch regressions across your marketing site or dashboard.

4) Visual regression testing that catches the subtle stuff

CSS is slippery. A refactor that looks fine in one browser can shift a component by 2px elsewhere. With Pest v4 you can snapshot pages and compare them to a baseline:

$pages = visit(['/', '/dashboard', '/settings']);
$pages->assertScreenshotsMatches(); 

Under the hood, Pest generates diffs that make UI changes obvious, so you can keep merging confidently even across large, multi-team codebases. Tip: agree a baseline review process with your team to avoid false positives on intentional UI updates.

5) Parallelism & sharding: speed that scales with your team

Pest’s parallel runner has been great for unit tests; now browser tests benefit too. On CI, sharding lets you split the suite across multiple jobs (great when test counts explode). Example GitHub Actions snippet:

strategy:
  matrix:
    shard: [1, 2, 3, 4]

name: Tests (Shard ${{ matrix.shard }}/4)

steps:
  - name: Run tests
    run: ./vendor/bin/pest --parallel --shard=${{ matrix.shard }}/4

Combine this with Playwright’s concurrency to slash end-to-end runtime while keeping the test code identical locally.

6) Type Coverage: now ~2× faster (and instant on reruns)

If you track type safety, v4’s new type coverage engine is a sleeper hit: roughly 2× faster on first pass and instant for subsequent runs. It also supports sharding, so you can check types as aggressively as tests without blowing up CI minutes.

composer require pestphp/pest-plugin-type-coverage --dev
./vendor/bin/pest --type-coverage --min=90

Docs for setup and options live in the Type Coverage guide.

7) Profanity checks, CI-aware skips, and other niceties

  • Profanity checker: keep the codebase professional by flagging offensive language in comments/test names.

  • composer require pestphp/pest-plugin-profanity --dev then run ./vendor/bin/pest --profanity.

  • Skip by environment: ->skipLocally() and ->skipOnCi() help you mark brittle or expensive tests appropriately.

  • Architecture expectations: more batteries included (e.g., detect suspicious characters), plus a new toBeSlug expectation.

  • Built on PHPUnit 12: you get the latest PHPUnit features under the hood with Pest’s nicer syntax on top.

8) Requirements & compatibility for Laravel 12

Before you upgrade:

  • PHP 8.3+ is required for Pest v4.

  • The Laravel plugin requires Laravel ^11.45.2 | ^12.25.0 and Pest ^4.0. If you’re on Laravel 12, you’re in the sweet spot.

Fresh install (Laravel 12 project)

# If you’re moving from pure PHPUnit:
composer remove phpunit/phpunit

# Install Pest and the Laravel plugin (+ init scaffolding)
composer require pestphp/pest --dev --with-all-dependencies
composer require pestphp/pest-plugin-laravel --dev
php artisan pest:install

# Add browser testing
composer require pestphp/pest-plugin-browser --dev
npm install playwright@latest
npx playwright install

The official install docs also recommend ignoring the screenshot output directory from your VCS.

9) Upgrading an existing Pest 3 suite to v4

The upgrade path is straightforward:

Bump dependencies in composer.json:

{
  "require-dev": {
    "pestphp/pest": "^4.0",
    "pestphp/pest-plugin-laravel": "^4.0"
  }
}

Ensure PHP 8.3+ in your runtime/CI images. Install the Browser plugin and Playwright (see steps above) if you’ll adopt the new browser features. Run your suite and address deprecations or expectation name changes as flagged by Pest.

Tip: If you keep a large Dusk suite, consider porting high-value flows to Pest Browser tests first—the syntax is tighter and integration with your existing Laravel test helpers is excellent.

10) Laravel-centric recipes you can lift into your app today

A. Full login flow with assertions + debug/tinker

use App\Models\User;
use Illuminate\Support\Facades\Event;

it('authenticates and lands on the dashboard', function () {
    Event::fake();

    $user = User::factory()->create([
        'email' => 'dev@example.com',
        'password' => 'password', // hashing handled in factory or use Hash::make()
    ]);

    visit('/')
        ->click('Log in')
        ->type('email', $user->email)
        ->type('password', 'password')
        ->press('Log in')
        ->assertSee('Dashboard')
        ->tinker(); // spin up a tinker session in context to poke around
});

The tinker() call is incredible for diagnosing failing end-to-end flows without bouncing between tools.

B. Cross-route smoke test with device + theme coverage

test('public pages are clean in multiple modes', function () {
    $pages = visit(['/', '/pricing', '/blog'])
        ->on()->iPhone15()
        ->inDarkMode();

    $pages->assertNoSmoke();
});

Keep this in a nightly CI job to catch sneaky frontend regressions early.

C. Visual baseline for the homepage

it('homepage layout stays consistent', function () {
    visit('/')->assertScreenshotsMatches();
});

Run visual tests when you touch global CSS, typography, or design tokens.

11) CI setup notes for Laravel 12 + Playwright

On GitHub Actions, you’ll need Node set up and to install Playwright browsers:

- uses: actions/setup-node@v4
  with:
    node-version: lts/*

- name: Install frontend deps
  run: npm ci

- name: Install Playwright browsers
  run: npx playwright install --with-deps

- name: Run Pest
  run: ./vendor/bin/pest --parallel

That last --with-deps flag pulls system dependencies many Linux runners need for headless browsers.

12) Good practices to keep browser tests fast and stable

  • Use data-test attributes for selectors. Text-based selectors are fine for quick spikes; attributes are more resilient.

  • Seed minimal state (factories + RefreshDatabase) per test. Avoid long UI setup chains when a direct factory call is cleaner.

  • Prefer headless locally, switch to --debug or ->debug() only when investigating failures.

  • Shard heavy suites on CI; even two shards can halve elapsed time. Combine with --parallel.

  • Baseline your visuals deliberately: PR #1 sets the ground truth; later PRs only update snapshots when changes are intentional.

13) Should you upgrade now?

If you’re on Laravel 12 and PHP 8.3+, upgrading to Pest v4 is low friction and immediately useful—particularly if you’ve been juggling a separate browser suite or skipping end-to-end tests entirely. Start by converting 2–3 high-value flows (auth, checkout, onboarding) to Pest Browser tests and add a smoke test over public routes. You’ll get quick confidence without a big rewrite.

14) Further reading & official docs

  • Pest v4 announcement (feature overview, code examples, links).

  • Browser testing docs (full API: assertions, interactions, debugging).

  • Laravel News coverage (context from Laracon, highlights).

  • Install & upgrade guides (requirements, Composer bumps).

  • Pest Laravel plugin (Packagist) for exact version constraints (Laravel 12 support)

Final thoughts

Pest v4 delivers what many Laravel teams have wanted for years: a single, elegant testing experience from unit to end-to-end. The syntax is tidy, the ergonomics are right, and the ecosystem support (parallelism, sharding, snapshots, type coverage) makes scaling tests with your product and team far more realistic. If you’ve been “meaning to add browser tests someday,” this is the moment to do it.

Happy testing.