skillv1.0.0
pest-testing
Pest PHP testing framework patterns: expectations, datasets, architecture tests, mocking
pestphplaraveltesting
Install
$npx autoskills --items pest-testing
Or scan + install everything matching your stack with npx autoskills.
Source
View on GitHubPest Testing
Pest is PHPUnit with a saner DX. Same engine, less ceremony. But the ergonomics tempt you into bad shapes โ this skill covers the patterns that scale.
Test file shape
<?php
use App\Models\User;
it('creates a user', function () {
$user = User::factory()->create(['email' => 'a@b.com']);
expect($user->email)->toBe('a@b.com');
});
it('rejects duplicate emails', function () {
User::factory()->create(['email' => 'a@b.com']);
expect(fn () => User::factory()->create(['email' => 'a@b.com']))
->toThrow(QueryException::class);
});it()overtest()for behavior-focused names.it('does X')reads as a sentence.test()is fine for pure helpers / non-behavioral cases.- One assertion focus per test. Multiple
expect()lines are fine, but they should all assert the same conceptual outcome.
Expectations chain
expect($user)
->name->toBe('Alice')
->email->toEndWith('@example.com')
->isAdmin->toBeFalse();Higher-order chaining keeps tests dense without losing clarity. Use for:
- Object shape:
expect($order)->total->toBe(100)->status->toBe('paid'); - Collection contents:
expect($users)->toHaveCount(3)->each->isActive->toBeTrue();
Datasets
it('validates emails', function (string $email, bool $valid) {
expect((new EmailValidator)->isValid($email))->toBe($valid);
})->with([
['alice@example.com', true],
['no-at-sign', false],
['@no-local.com', false],
]);- Named datasets for reuse:
// tests/Datasets/Emails.php dataset('emails', [ 'valid' => ['alice@example.com', true], 'no @' => ['no-at-sign', false], ]); it('validates emails', function ($email, $valid) { ... })->with('emails'); - Multiple
with()calls = cross product. Useful for matrix tests (e.g.,[1, 2, 3] ร ['a', 'b']= 6 cases). - Failure messages include dataset key when you use the named form โ much easier to debug.
Hooks
beforeEach(function () {
$this->user = User::factory()->create();
});
afterEach(function () {
Cache::flush();
});$thisworks โ Pest is closures-on-PHPUnit-cases under the hood.beforeAll/afterAllfor expensive setup, but it runs once for the whole file โ beware shared state.
Architecture tests
// tests/Architecture/CleanlinessTest.php
arch('controllers do not use models directly')
->expect('App\Http\Controllers')
->not->toUse('App\Models');
arch('no debug calls in production code')
->expect(['dd', 'dump', 'var_dump', 'ray'])
->not->toBeUsed();Architecture tests are pest-unique. Use them to enforce structural rules CI can catch.
Mocking (Mockery)
$mock = $this->mock(PaymentGateway::class)
->shouldReceive('charge')
->once()
->with(100)
->andReturn(new ChargeResult(success: true))
->getMock();- Prefer real implementations + DB transactions for Laravel tests. Mocks are for boundaries you don't control (third-party APIs).
->expects('method')is the typed variant โ better failure messages.
Laravel-specific helpers
// HTTP test
$this->actingAs($user)
->postJson('/api/posts', ['title' => 'X'])
->assertCreated()
->assertJson(['title' => 'X']);
// DB assertions
$this->assertDatabaseHas('posts', ['title' => 'X']);
$this->assertDatabaseCount('posts', 1);
// Mail/Queue/Event fakes
Mail::fake(); ...; Mail::assertSent(WelcomeEmail::class);
Queue::fake(); ...; Queue::assertPushed(ProcessOrder::class);Speed
RefreshDatabaseis the default for Feature tests โ runs migrations once, transactions roll back per test.DatabaseTransactionsif migrations are slow โ assumes the DB is already in a usable state.- Use SQLite in-memory in CI when feasible:
DB_CONNECTION=sqlite, DB_DATABASE=:memory:. - Parallel:
php artisan test --parallelโ uses ParaTest under the hood. Big win on multi-core CI.
What to avoid
- One giant
it('handles all the things')โ split per behavior. - Asserting on internal state instead of observable outcome โ couples tests to implementation.
- Sleeping in tests โ use Carbon::setTestNow() or queue fakes instead.
- Skipping flaky tests with
->skip()โ fix or delete. A skipped test rots. - Snapshot tests for everything โ snapshots are great for stable output, terrible for output you change often.