Pest Control
Rule 0 — Hard Stop
Pest is NOT installed in this project and must never be used.
This project uses PHPUnit 12+ exclusively.
Never write, suggest, or accept:
it('description', fn () => ...)test('description', fn () => ...)describe('group', fn () => ...)uses(SomeClass::class)expect($value)->toBe(...)beforeEach(fn () => ...)afterEach(fn () => ...)pest()configuration
Rule 1 — Correct Test Class Pattern
Every test MUST be a class extending one of the three base classes:
// Company panel tests
class FooTest extends AbstractCompanyPanelTestCase
{
#[Test]
public function it_does_something(): void
{
// ...
}
}
// Admin panel tests
class BarTest extends AbstractAdminPanelTestCase
{
#[Test]
public function it_does_something(): void
{
// ...
}
}
// Pure unit tests (no DB, no framework boot)
class BazTest extends AbstractTestCase
{
#[Test]
public function it_does_something(): void
{
// ...
}
}
Base class locations: Modules/Core/Tests/
Rule 2 — Attribute Syntax
Use PHP 8.1+ attributes for test metadata:
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\CoversClass;
#[Test]
public function it_creates_an_invoice(): void {}
#[Test]
#[DataProvider('invoiceDataProvider')]
public function it_validates_invoice_fields(array $data, string $error): void {}
Never use /** @test */ docblock annotations — use #[Test] attributes.
Rule 3 — Assertion Style
Use PHPUnit assertions, not Pest chains:
// Correct
$this->assertSame('expected', $actual);
$this->assertDatabaseHas('invoices', ['status' => 'paid']);
$this->assertCount(3, $results);
// Wrong — Pest chain
expect($actual)->toBe('expected');
expect($results)->toHaveCount(3);
Rule 4 — Livewire Testing
Filament/Livewire tests use the Livewire facade directly:
use Livewire\Livewire;
Livewire::actingAs($this->user)
->test(ListInvoices::class, ['tenant' => 'ivplv2'])
->assertSuccessful();
Or the base class helper:
$this->testLivewire(ListInvoices::class)->assertSuccessful();
Rule 5 — File Placement
Modules/<Name>/Tests/Unit/ ← AbstractTestCase, no DB
Modules/<Name>/Tests/Feature/ ← AbstractCompanyPanelTestCase or AbstractAdminPanelTestCase
PHPUnit discovers tests via phpunit.xml:
<testsuite name="Unit"> <directory>Modules/*/Tests/Unit</directory> </testsuite>
<testsuite name="Feature"><directory>Modules/*/Tests/Feature</directory></testsuite>
Rule 6 — Pest Elimination Checklist
When asked to eliminate Pest from a codebase, check and fix all of the following:
composer.json
- Remove
"pestphp/pest-plugin": truefromconfig.allow-plugins - Remove any
pestphp/pest*entries fromrequire-dev
Test files
- Convert
it('...', fn () => ...)→ class method with#[Test]attribute - Convert
test('...', fn () => ...)→ class method with#[Test]attribute - Remove all
uses(...)declarations - Replace
expect(...)->toBe(...)chains with$this->assertSame(...) - Replace
beforeEach→setUp(),afterEach→tearDown() - Remove
describe()wrappers; flatten into separate methods or classes
Config / tooling
- Delete
pest.phportests/Pest.phpif present - Remove any
--pestflag from CI workflow commands - Update
.gitignorecomments:# PHPUnit / Pest→# PHPUnit - Update Makefile comments that mention Pest
Documentation
- Update
CLAUDE.mdtesting section to state PHPUnit-only - Update any README or CONTRIBUTING docs that mention Pest
Rule 7 — Conversion Reference
| Pest | PHPUnit equivalent |
|---|---|
it('desc', fn() => ...) |
#[Test] public function it_desc(): void |
test('desc', fn() => ...) |
#[Test] public function test_desc(): void |
expect($x)->toBe($y) |
$this->assertSame($y, $x) |
expect($x)->toEqual($y) |
$this->assertEquals($y, $x) |
expect($x)->toBeTrue() |
$this->assertTrue($x) |
expect($x)->toBeFalse() |
$this->assertFalse($x) |
expect($x)->toBeNull() |
$this->assertNull($x) |
expect($x)->toBeEmpty() |
$this->assertEmpty($x) |
expect($x)->toHaveCount(n) |
$this->assertCount(n, $x) |
expect($x)->toContain($y) |
$this->assertContains($y, $x) |
expect($x)->toMatchArray([...]) |
$this->assertEquals([...], $x) |
expect($x)->toBeInstanceOf(Cls::class) |
$this->assertInstanceOf(Cls::class, $x) |
beforeEach(fn() => ...) |
protected function setUp(): void |
afterEach(fn() => ...) |
protected function tearDown(): void |
uses(RefreshDatabase::class) |
use RefreshDatabase; inside the class |
dataset(...) |
public static function provider(): array + #[DataProvider('provider')] |