Testing PHP Applications

Pest Advanced Features

Pest's advanced features solve different testing problems. Architecture tests protect structural rules. Profiling identifies slow tests. Parallel execution reduces elapsed time when tests are isolated. Snapshots make complex output changes visible. Browser tests verify a small number of journeys through the rendered interface.

Do not enable all of them because they are available. Start with a specific problem, choose the smallest suitable feature, and confirm that the team can maintain the result.

Protect Boundaries With Architecture Tests

An architecture test inspects the codebase rather than running a business workflow. It can enforce naming, inheritance, namespace, and dependency rules that would otherwise rely on code review.

For example, suppose models should only depend on Laravel's database namespace:

PHP example
<?php

arch('models use the database layer only')
    ->expect('App\\Models')
    ->toOnlyUse('Illuminate\\Database');

That rule is intentionally strict. A real model may also need project contracts, casts, or framework concerns, so write the rule around an architectural decision your application genuinely follows. A broad rule copied from another project creates false failures and is soon ignored.

Good architecture rules are:

  • narrow enough to explain in one sentence;
  • connected to a documented design decision;
  • reviewed when the architecture changes;
  • kept with the rest of the test suite.

Architecture tests complement runtime tests. They can prove that a namespace does not cross a boundary, but they cannot prove that an order total is correct or that a user can log in.

Profile Before Optimising

Run the suite with profiling before guessing which tests are slow:

./vendor/bin/pest --profile

The profile highlights tests that deserve investigation. Common causes include rebuilding the application repeatedly, unnecessary database work, real network calls, expensive password hashing, and browser setup in tests that do not need a browser.

Treat timing results as evidence, not as an instruction to delete valuable coverage. First ask whether the same behaviour can be checked at a lower level, whether setup can be made cheaper without sharing mutable state, and whether an external service should be replaced by a controlled fake.

Prepare For Parallel Execution

Pest can run tests in parallel:

./vendor/bin/pest --parallel

You can also set the process count:

./vendor/bin/pest --parallel --processes=4

Parallel execution is only reliable when tests are independent. Check the suite for:

  • fixed database names or records shared by every process;
  • hard-coded temporary filenames;
  • shared cache keys, queues, ports, or mailboxes;
  • tests that depend on execution order;
  • global state that is changed but not restored;
  • external sandbox accounts reused concurrently.

Each worker needs isolated resources or process-specific identifiers. A faster suite that fails intermittently is worse than a slower deterministic suite. Trial parallel execution locally and in CI, repeat it enough times to expose collisions, and keep the process count appropriate for the CI machine.

Use Filtering For A Tight Feedback Loop

During development, run the smallest relevant slice:

./vendor/bin/pest --filter="an authenticated user can update their email"

Then run the complete suite before considering the change finished. Pest's documented --filter option is portable across projects. Some editors, shell tools, or repository scripts can rerun that command when files change, but there is no reason to assume every Pest project has the same watch command. Check the project's scripts before documenting or using one.

Filtering is for feedback speed, not proof that unrelated behaviour still works.

Snapshot Stable, Reviewable Output

A snapshot stores an approved representation and compares future output with it:

PHP example
<?php

it('renders the invoice summary', function () {
    $html = renderInvoiceSummary(
        customer: 'Ada Lovelace',
        totalInPence: 1299,
    );

    expect($html)->toMatchSnapshot();
});

Snapshots work well for structured output that is meaningful but tedious to assert field by field. They work badly when the output contains timestamps, random identifiers, machine-specific paths, unstable ordering, or an entire page that changes frequently.

Normalise unstable values before taking the snapshot. Keep explicit assertions for important business facts, because a snapshot shows that output changed but does not explain which requirement matters.

When an intentional change affects snapshots, update them with:

./vendor/bin/pest --update-snapshots

Review the resulting diff as carefully as a source-code change. Never treat snapshot updates as generated noise to approve automatically.

Keep Browser Coverage Narrow

Pest browser testing is provided through a separate plugin:

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

The plugin uses Playwright and adds browser-oriented APIs such as visit():

PHP example
<?php

it('allows a customer to sign in', function () {
    visit('/login')
        ->type('email', 'customer@example.com')
        ->type('password', 'correct-horse-battery-staple')
        ->press('Sign in')
        ->assertSee('Your account');
});

The exact selectors and setup must match the application. Seed a known user, isolate browser data, wait for visible application states instead of arbitrary sleeps, and retain screenshots or traces when CI fails.

Use browser tests for a few high-value journeys across the real interface. Keep validation rules, authorization decisions, and domain calculations in faster lower-level tests.

Adopt Features In A Deliberate Order

A practical rollout for a slow, drifting suite is:

  1. Profile the current suite and record a baseline.
  2. Remove order dependencies and shared mutable resources.
  3. Trial parallel execution and measure both speed and reliability.
  4. Add one architecture rule for a boundary the team has agreed to protect.
  5. Snapshot only stable output with an assigned reviewer.
  6. Add browser coverage only for a critical journey not adequately covered below the UI.

Each step should solve a named problem and have a clear owner. Advanced test tooling earns its place when it produces faster feedback or catches a meaningful class of regression without making the suite unpredictable.

Practice

Practice: Plan An Advanced Pest Rollout

A project's Pest suite takes 14 minutes in CI. Tests sometimes collide through shared database records, classes in App\\Domain have started importing infrastructure implementations, invoice HTML is difficult to review with individual string assertions, and login is the only journey the team considers business-critical.

Write a staged rollout plan that:

  • profiles the suite before changing it;
  • makes shared resources safe before enabling parallel execution;
  • includes the commands used to trial and tune parallel runs;
  • adds one concrete architecture test for the domain boundary;
  • explains what should be normalised before snapshotting invoice HTML;
  • keeps explicit assertions for important invoice facts;
  • limits browser coverage to the critical login journey;
  • describes how snapshot and browser failures will be reviewed in CI.

Do not solve every problem with a browser test or snapshot.

Show solution
./vendor/bin/pest --profile

Before adding workers, remove order dependencies and give each process separate database records, cache keys, queue names, and temporary files. Trial the suite repeatedly:

./vendor/bin/pest --parallel --processes=2
./vendor/bin/pest --parallel --processes=4

Choose the lowest process count that gives a useful speed improvement without exhausting the CI machine or causing intermittent collisions.

Protect the agreed domain boundary with a focused architecture test:

PHP example
<?php

arch('domain code does not depend on infrastructure')
    ->expect('App\\Domain')
    ->not->toUse('App\\Infrastructure');

Before snapshotting invoice HTML, replace random IDs and timestamps with deterministic values and ensure collections have stable ordering. Keep direct expectations for the invoice number, currency, and total so a reviewer knows which business facts are essential. Use toMatchSnapshot() for the remaining stable representation, and update snapshots only with:

./vendor/bin/pest --update-snapshots

The snapshot diff must be reviewed as part of the code change.

Add one browser test for successful login, backed by a known isolated user. Keep invalid credentials, authorization, validation, and session rules in lower-level tests. Configure CI to retain a screenshot or trace on browser failure and ensure those artifacts do not expose credentials or personal data.

Finally, run the full suite without a filter. The rollout succeeds only if runtime improves while repeated CI runs remain deterministic.