| name | pest |
| description | Pest testing framework conventions and practices. Invoke whenever task involves any interaction with Pest โ writing function-style tests (test/it/describe), the expect() expectation API, datasets, hooks, architecture/mutation/type-coverage/browser testing, Pest.php configuration, or running the pest CLI. |
Pest
Tests are functions, not classes. Write the expectation, not the ceremony โ expect($x)->toBe($y), and let the
description say what the code does.
Pest is a testing framework built on the PHPUnit engine: closures instead of test-case methods, an expect()
expectation API instead of $this->assert*, and first-class architecture, mutation, type-coverage, and browser testing.
$this inside a test closure is a PHPUnit TestCase, so the full PHPUnit assertion and test-double API stays
available. All patterns target Pest 4 on PHP 8.5+ (it runs on the PHPUnit 12 engine; its own floor is PHP 8.3).
This skill governs Pest-specific decisions. It does not restate universal testing philosophy (test behavior not
implementation, arrange-act-assert, isolation, mock at boundaries), the PHPUnit assertion/double API, coverage
attributes, or phpunit.xml internals โ those live in the phpunit skill and apply unchanged here.
References
- Full expectation catalog + modifiers, higher-order & custom expectations โ
[
${CLAUDE_SKILL_DIR}/references/expectations.md] โ every expect() method by category,
not/and/each/sequence, expect()->extend()/intercept()/pipe()
- Architecture testing โ
arch() rules and presets โ [${CLAUDE_SKILL_DIR}/references/architecture.md] โ all arch
expectations, php/security/laravel/strict presets, modifiers, wildcards
- Mutation, type coverage, snapshot, stress, browser testing โ
[
${CLAUDE_SKILL_DIR}/references/advanced-features.md] โ opt-in modes, their plugins, drivers, and CLI flags
- Pest.php configuration + full CLI flag catalog + PHPUnit migration โ
[
${CLAUDE_SKILL_DIR}/references/configuration.md] โ pest()->extend/use/in/group, global hooks, every run flag,
Drift
Test Structure
Install with composer require pestphp/pest --dev --with-all-dependencies (remove phpunit/phpunit first). Pest's
--init scaffolds tests/Unit/ and tests/Feature/, a base tests/TestCase.php, and tests/Pest.php. Files are
suffixed *Test.php.
test() / it() / describe()
test('description', fn) โ the base form. Describe the behavior:
test('returns empty collection when no results').
it('description', fn) โ same as test() but prefixes the output with "it", reading as a sentence:
it('rejects an expired token'). Pick one convention per file/suite and keep it consistent.
describe('group', fn) โ groups related tests; hooks declared inside it scope to that block. Nest for sub-groups.
it('sums two integers', function () {
expect(sum(1, 2))->toBe(3);
});
describe('sum', function () {
it('adds integers', fn () => expect(sum(1, 2))->toBe(3));
it('adds floats', fn () => expect(sum(1.5, 2.5))->toBe(4.0));
});
- One concept per test. Multiple expectations are fine when they verify one behavior; split independent behaviors.
- No test classes. Don't write
extends TestCase in a Pest suite โ that is the phpunit skill's paradigm.
- Helper logic becomes a plain function (in the test file,
tests/Helpers.php, or tests/Helpers/), not a protected
method. Call test() inside a helper to reach the instance otherwise available as $this.
Expectations
expect($value) opens a chain; chain expectations directly, invert with not, switch values with and().
expect($user->name)->toBe('Nuno')
->and($user->roles)->toBeArray()->toHaveCount(2)
->and($user->isActive())->toBeTrue();
- Prefer
toBe() (strict ===) over toEqual() (loose ==). toBe() catches type bugs; on objects it asserts
the same instance. Use toEqual() only to compare two distinct objects/arrays by value.
- Prefer the
expect() API over $this->assert*. PHPUnit assertions remain valid ($this->assertSame(3, $result))
and are the escape hatch for assertions Pest has no expectation for.
- Common expectations:
toBe, toBeTrue/toBeFalse, toBeNull, toBeInstanceOf, toBeArray, toHaveCount,
toContain, toHaveKey, toMatchArray, toBeGreaterThan, toThrow. Full catalog โ expectations reference.
expect([...])->each->toBeInt() applies an expectation to every element; ->sequence(...) checks elements
positionally.
- Repeated expectation logic โ a custom expectation via
expect()->extend('toBeWithinRange', fn ($min, $max) => ...) in
Pest.php. See the expectations reference.
dd(), ddWhen(), and ray() are debugging modifiers โ never leave them in committed tests.
Hooks
beforeEach(fn) โ runs before every test in the file; the place to build the SUT and assign $this->property
shared across the file's tests.
afterEach(fn) โ runs after every test; only needed to release external resources.
beforeAll(fn) / afterAll(fn) โ run once per file. $this is not available in either (no instance exists).
$this inside test and beforeEach/afterEach closures is the bound TestCase โ properties set in beforeEach
persist into the test.
- Hooks declared inside a
describe() block scope to that block. Suite-wide hooks go in Pest.php (configuration
reference).
- Per-test cleanup: chain
->after(fn) onto a single test.
beforeEach(function () {
$this->repository = new UserRepository();
});
it('creates a user', function () {
expect($this->repository->create())->toBeInstanceOf(User::class);
});
Datasets
Datasets are Pest's data providers โ run one test across many inputs with ->with().
it('validates emails', function (string $email) {
expect($email)->toContain('@');
})->with([
'gmail' => 'enunomaduro@gmail.com',
'laravel' => 'taylor@laravel.com',
]);
- Use named (keyed) datasets โ keys produce readable failure descriptions.
:dataset in the description
interpolates the key at that position.
- Multiple arguments: provide an array of argument arrays; closure parameters must be typed.
- Shared datasets:
dataset('emails', [...]) in tests/Datasets/ (or a scoped Datasets.php), referenced by name:
->with('emails'). Associative-array datasets map by parameter name regardless of order.
- Lazy datasets: pass a closure or generator to
->with() (->with(fn (): array => range(1, 99))) to build large
or computed datasets at run time.
- Bound datasets: wrap values in closures (
fn () => User::factory()->create()) to resolve them after
beforeEach() โ required when a value depends on test setup (e.g. a seeded model). The bound parameter must be fully
typed.
- Stack
->with(...)->with(...) to combine datasets as a cartesian product. ->repeat(n) re-runs a test n times.
->with() on a describe() block (or beforeEach()->with([...]) inside it) feeds the dataset to every test in the
block.
Exceptions
Attach exception expectations to the test, or assert on a closure with toThrow():
it('rejects division by zero', function () {
Calculator::divide(1, 0);
})->throws(DivisionByZeroError::class, 'cannot divide by zero');
it('does not throw on valid input', function () {
Calculator::divide(10, 2);
})->throwsNoExceptions();
->throws($class) / ->throws($class, 'message') / ->throws('message') โ message match is a substring.
->throwsIf($cond, ...) / ->throwsUnless($cond, ...) โ conditional expectation.
->throwsNoExceptions() โ assert the body runs clean (gives the test a real assertion).
expect(fn () => ...)->toThrow(...) โ inline form when you need it mid-chain.
->fails() / ->fails('message') โ assert the test itself fails (message is a substring). Fail explicitly from
inside with $this->fail('reason').
Filtering, Skipping, Focus
->skip() / ->skip('reason') / ->skip($condition, 'reason') โ skip a test; pass a closure for the condition
to defer evaluation to after beforeEach. Environment/OS/PHP variants: skipOnCi(), skipLocally(),
skipOnWindows(), skipOnMac(), skipOnLinux(), onlyOnLinux(), skipOnPhp('>=8.5'). beforeEach()->skip() skips
the whole file.
->todo() โ placeholder for a planned test.
->group('integration') โ tag a test/describe/file; run with --group. Assign whole directories to groups in
Pest.php.
->only() โ focus a single test (or pest()->only() for a whole file) during development. Remove before
committing; CI should run with --ci to neutralize stray focus.
->depends('parent', ...) โ run only after the named test(s) pass; parent return values arrive as arguments in
dependency order. Reference it() tests by their full "it ..." description.
->flaky() / ->flaky(tries: 5) โ auto-retry a test that fails for external reasons (default 3 tries); does not
retry skipped tests or expected exceptions.
Mocking
Pest has no mocking library of its own โ use Mockery (recommended) or PHPUnit's built-in test doubles via $this.
composer require mockery/mockery --dev
it('does not call the API on a dry run', function () {
$client = Mockery::mock(PaymentClient::class);
$client->shouldReceive('post')->never();
(new BookRepository($client))->buy(dryRun: true);
});
Mockery::mock(Class::class) then ->shouldReceive('method') with ->with(...), ->andReturn(...),
->andThrow(...), and count expectations (->once(), ->times(3), ->atLeast()->times(n)).
- Stub vs mock, mock-at-boundaries, don't-mock-what-you-own โ the doctrine lives in the
phpunit skill's
mocking section. Apply it here; default to stubbing returns and only verify interactions when the side effect is the
behavior under test.
Higher-Order Testing
When a test body is a single chain on $this or one lazy expect(), drop the closure:
it('returns 200 on the homepage')->get('/')->assertStatus(200);
it('stores the name')
->expect(fn () => User::create(['name' => 'Nuno'])->name)
->toBe('Nuno');
Pass a closure to expect() for lazy evaluation (the value is built at run time); ->defer(fn () => ...) runs the
closure at test time and chains assertions on its result. Hooks can be higher-order too
(beforeEach()->withoutMiddleware()), and dataset values are passed into expect()/defer() closures. Use this style
only when it reads clearer than an explicit closure โ don't contort a multi-step test to fit it.
Architecture Testing
arch() asserts structural rules over namespaces without imperative test code. Add a few to any project:
arch('no debug helpers')->expect('App')->not->toUse(['dd', 'dump', 'die']);
arch('value objects are final')->expect('App\ValueObjects')->toBeFinal()->toBeReadonly();
arch()->preset()->php();
arch()->preset()->security();
Presets (php, security, laravel, strict) bundle common rules. Full expectation and modifier catalog โ
architecture reference.
Advanced Modes
Opt-in testing modes โ reach for the advanced-features reference when a task uses one:
- Mutation testing โ
covers(Class::class) + --mutate scores whether tests actually catch behavior changes (needs
Xdebug/PCOV).
- Type coverage โ
--type-coverage measures type-declaration completeness (pest-plugin-type-coverage).
- Browser testing โ
visit('/')->click(...)->assertSee(...) drives a real browser (pest-plugin-browser +
Playwright).
- Snapshot testing โ
expect($output)->toMatchSnapshot() for large, stable structural output.
- Stress testing โ
stress() / pest stress <url> load-tests via k6 (pest-plugin-stressless).
Running Tests
./vendor/bin/pest
./vendor/bin/pest tests/Unit/SumTest.php
./vendor/bin/pest --filter "sums"
./vendor/bin/pest --group integration
./vendor/bin/pest --parallel
./vendor/bin/pest --coverage --min=90
./vendor/bin/pest --dirty
./vendor/bin/pest --bail
./vendor/bin/pest --retry
Full flag catalog โ configuration reference.
Application
When writing tests: apply all conventions silently โ don't narrate each rule. Match the project's existing Pest
style (test vs it, file layout, dataset conventions). If an existing codebase contradicts a convention, follow the
codebase and flag the divergence once.
When reviewing tests: cite the specific issue and show the fix inline. Don't lecture โ state what's wrong and how to
fix it.
Bad: "Pest best practices recommend the expectation API over PHPUnit assertions because it reads more fluently..."
Good: "$this->assertSame(3, $x) โ expect($x)->toBe(3)"
Integration
- Which testing skill applies: if the project uses Pest (a
tests/Pest.php exists, pestphp/pest is in
composer.json, tests are function-style), this skill governs. If tests are PHPUnit class-based (extends TestCase),
the phpunit skill governs. Don't mix paradigms within one suite; to convert, use Drift (configuration reference).
- The
php skill governs language choices (types, naming, OOP) โ including the base TestCase and any helpers.
- The
the-coder skill governs workflow (discovery, planning, verification).
Tests are functions. Prefer expect() over assertions, toBe() over toEqual(), and a named dataset over a copied
test. When in doubt, mock less.