Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Pest Laravel Package

pestphp/pest

Pest is an elegant PHP testing framework with a clean, expressive syntax built on PHPUnit. Write readable tests with higher-order expectations, datasets, and architecture testing, plus first-class Laravel support.

View on GitHub
Deep Wiki
Context7

Getting Started

Pest is a thin, ergonomic layer over PHPUnit. Your existing phpunit.xml, test suites, and PHPUnit assertions keep working — you just get a functional, expressive API on top. On a Laravel project this is nearly a drop-in.

  1. Install (Laravel projects):

    composer require pestphp/pest pestphp/pest-plugin-laravel --dev --with-all-dependencies
    ./vendor/bin/pest --init
    

    --init scaffolds tests/Pest.php and hooks your existing tests/TestCase.php. Laravel starter kits ship with Pest already, so often you only need step 2.

  2. Where to look first — in this order:

    • tests/Pest.php — the single most important file. It's where uses(...)->in(...) binds your Laravel TestCase and traits to test directories. Everything breaks or works based on this file.
    • phpunit.xml — still the source of truth for test suites, env vars (DB_CONNECTION=sqlite, DB_DATABASE=:memory:), and coverage include paths.
    • tests/Feature, tests/Unit — the directories you'll write in.
  3. First use case — convert one existing PHPUnit feature test. Take a class-based test and rewrite:

    // tests/Feature/InvoiceTest.php
    use App\Models\Invoice;
    use App\Models\User;
    
    it('marks an invoice as paid', function () {
        $user = User::factory()->create();
        $invoice = Invoice::factory()->for($user)->create();
    
        $this->actingAs($user)
            ->post("/invoices/{$invoice->id}/pay")
            ->assertRedirect();
    
        expect($invoice->fresh()->status)->toBe('paid');
    });
    

    Run ./vendor/bin/pest --filter=paid. Notice $this is the Laravel TestCase (thanks to tests/Pest.php), so actingAs, post, and assertDatabaseHas are all available. That's the whole migration story: same harness, less boilerplate.

Implementation Patterns

Bind the framework harness once, in tests/Pest.php:

uses(Tests\TestCase::class, RefreshDatabase::class)->in('Feature');
uses(Tests\TestCase::class)->in('Unit');

This is the first thing to get right on any Laravel codebase — it turns every closure test into a full application boot.

Hooks over base-class setUp: beforeEach, afterEach, and afterAll can be declared globally (in Pest.php), per directory, per describe block, or per file. Scope them tightly — a global beforeEach that seeds data is a classic source of mysterious slowdowns.

describe('subscriptions', function () {
    beforeEach(fn () => $this->user = User::factory()->create());

    it('renews on the billing date', function () { /* ... */ });
});

Dataset values are injected into hooks too, so beforeEach(fn (string $plan) => ...) with ->with(['pro', 'team']) works.

Datasets for table-driven tests: define shared sets in tests/Datasets/*.php and reference them by name with ->with('datasetName'). Prefer named arguments in datasets — the repeat/retry + named-dataset-argument interactions have been patched repeatedly, so pin your version.

it('validates emails', function (string $input, bool $valid) {
    expect(validator(['email' => $input], ['email' => 'email'])->passes())->toBe($valid);
})->with([
    'valid'   => ['[email protected]', true],
    'missing' => ['not-an-email', false],
]);

Expectations vs. $this assertions: use expect() for value assertions (toBe, toBeInstanceOf, toMatchArray, toHaveCount), and $this->assert* / $this->get()... for HTTP/database/queue concerns. Both live in the same test — that's expected, not a smell. expect($user)->toHaveName('...') style higher-order expectations are also available via model accessors/methods.

Architecture tests pay for themselves immediately. Put them in tests/ArchTest.php and run them in CI:

arch()->preset()->laravel()->preset()->php()->preset()->security();
arch('no debugging leftovers')->expect(['dd', 'dump', 'ray', 'var_dump'])->not->toBeUsed();

preset()->laravel() encodes a lot of framework conventions (e.g. controllers suffixed correctly, no App\Http in providers). Adopt it first, then tighten with ->toExtend(), ->toUseTrait(), and wildcards — presets are opinionated and will flag legacy code, so roll them out per-directory rather than big-bang.

Day-to-day loop:

  • --dirty — only run tests with uncommitted changes. The fastest inner-loop command.
  • --filter="checkout" / --group=payments — target by name or group (->group('payments')).
  • --profile — prints your slowest tests. Run it before blaming CI.
  • --parallel — Paratest under the hood; combine with per-process SQLite or a dedicated test DB.
  • --tia — test-impact analysis: runs only tests reachable from changed source lines. Excellent locally, needs a coverage driver (PCOV/Xdebug), and caches a graph. Don't gate merges on it.

CI patterns:

  • Coverage: --coverage --min=80 --compact (the --compact flag keeps output readable and hides >100% type coverage). Coverage requires PCOV/Xdebug; forked parallel runs can be memory-hungry.
  • Sharding: --shard=1/5 distributes by file count by default. For balanced wall-clock time, run ./vendor/bin/pest --parallel --update-shards once, commit tests/.pest/shards.json, and shard-by-time becomes automatic.
  • Flaky tests: --flaky retries and reports rather than failing the build outright. Use it to identify flakiness, not to hide it permanently.
  • Snapshots: toMatchSnapshot() for serialized payloads/HTML; update with --update-snapshots, which respects --filter and file args.

Gotchas and Tips

  • "Call to undefined method …::get()" almost always means your test directory isn't covered by uses(Tests\TestCase::class)->in('...') in tests/Pest.php. Check the directory name — a nested folder like tests/Feature/Admin still matches in('Feature'), but a directory outside the listed ones does not.
  • Don't mix PHPUnit class tests and Pest closure tests in the same file. Both work in a project; a single file must pick one style.
  • Version pinning matters more than usual. Pest v5 tracks PHPUnit 13.3, v4 tracks PHPUnit 12.x. Security advisories in PHPUnit drove several Pest patch releases (2.36.1, 3.8.5, 4.3.2, 4.6.2) — treat composer update pestphp/pest as a security action, and keep phpunit/phpunit unpinned so Pest's constraints resolve.
  • --parallel + RefreshDatabase is the most common source of "random" CI failures. Each worker needs its own database (or SQLite file per process). In-memory SQLite with parallel workers works only because each worker is a separate process with its own connection — but any shared database, Redis, or cache store will collide. Use --parallel with a Laravel-specific DB per process, or keep the suite sequential.
  • Datasets and hooks: a skipped test from inside a beforeEach is now correctly treated as finished by the TIA engine, but historically has been a bug-rich area. Assert on hook behavior rather than assuming.
  • TIA fingerprints include the PHP minor version — switching from 8.3 to 8.4 invalidates the graph. Don't commit the cache; let CI rebuild or skip it entirely. If you see memory exhaustion while generating the TIA graph with PCOV, upgrade to ≥5.1.2.
  • Sharding exit codes with --shard combined with --parallel were fixed in 5.1.2, and shard filters could match classes outside their partition until 5.3.2. If you're sharding in CI, stay on a recent patch — a green shard matrix that silently skipped tests is worse than a red one.
  • Arch presets are noisy on legacy code and can be affected by namespaced function imports (a fixed bug, but a reminder that arch tests reflect the whole namespace map). Exclude database/migrations, generated code, and vendor-ish directories explicitly.
  • Extension points: expect()->extend() in Pest.php for domain-specific assertions, custom uses() trait bindings for shared behavior, and plugin packages (pest-plugin-laravel for artisan/actingAs/freezeTime, pest-plugin-mutation, pest-plugin-type-coverage, the v4 Browser plugin) rather than hand-rolling. Keep tests/Pest.php deterministic — it's loaded for every run, so no side effects, no network, no env() mutation there.
  • Debugging a single test in parallel: parallel output can interleave dd()/dumps. Run the specific test sequentially with --filter before reaching for a debugger.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
vento/laravel-airbrake
wfeller/laravel-batch
maco-studios/mage-os-installer
busarm/armie
siberfx/laravel-logger
kfteam1991/dcat-admin
dashed/dashed-laposta
dashed/dashed-ecommerce-montaportal
dashed/dashed-ecommerce-etsy
neohq/string-blade-compiler
log1x/acf-composer
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core
nodisrupt/laravel-agent
sierratecnologia/media-manager