Laravel 13 Testing — Pest PHP 4 & PHPUnit 12
Supports both Pest PHP 4 and PHPUnit 12. See Framework Detection below.
PHPUnit version note: Laravel 13 ships with phpunit/phpunit: ^12.5.12 in its default composer.json. All patterns in this skill are compatible with PHPUnit 11, 12, and 13.
Comprehensive testing guide for Laravel 13 applications. Contains 24 rules across 6 categories for writing fast, readable, and reliable tests. Supports both Pest PHP 4 and PHPUnit 12 (Laravel 13 default).
Framework Detection
Before writing or reviewing any test code, detect which testing framework the project uses:
Step 1 — Check composer.json
# Look for these in require-dev:
# "pestphp/pest" → Pest
# "phpunit/phpunit" (without pest) → PHPUnit
- If
pestphp/pest is present → use Pest syntax
- If only
phpunit/phpunit is present → use PHPUnit syntax
- If both are present → Pest takes priority (Pest runs on top of PHPUnit)
Step 2 — Check for tests/Pest.php
- If
tests/Pest.php exists → Pest is configured, use Pest syntax
Step 3 — If still unclear, ask the user
"I couldn't detect the testing framework. Does this project use Pest PHP or PHPUnit?"
Syntax Reference
Test Declaration
|
Pest |
PHPUnit |
| Test function |
test('...', fn() => ...) |
public function test_...(): void |
| Readable name |
it('...', fn() => ...) |
#[Test] public function it_...() |
| Grouping |
describe('...', fn() => ...) |
Test class name / nested classes |
| Trait application |
uses(RefreshDatabase::class) |
use RefreshDatabase; inside class |
| Before each |
beforeEach(fn() => ...) |
protected function setUp(): void |
| After each |
afterEach(fn() => ...) |
protected function tearDown(): void |
| Parameterised |
->with([...]) |
#[DataProvider] attribute |
| Global setup |
uses(...)->in('Feature') in Pest.php |
Base TestCase class |
Core assertions (identical in both frameworks)
assertStatus, assertJson, assertJsonPath, assertDatabaseHas, assertModelExists, actingAs, Mail::fake(), Queue::fake(), Event::fake(), Notification::fake(), Storage::fake() — all work the same in Pest and PHPUnit.
When to Apply
Reference these guidelines when:
- Writing feature or unit tests for Laravel
- Testing HTTP endpoints and API responses
- Creating factories and test data
- Asserting database state after operations
- Faking Mail, Queue, Notification, or Event facades
- Testing authenticated routes and API tokens
- Organising tests with describe blocks, datasets, or test classes
Rule Categories by Priority
| Priority |
Category |
Impact |
Prefix |
| 1 |
HTTP & Feature Tests |
CRITICAL |
http- |
| 2 |
Model Factories |
CRITICAL |
factory- |
| 3 |
Database Assertions |
HIGH |
db- |
| 4 |
Faking Services |
HIGH |
fake- |
| 5 |
Authentication Testing |
HIGH |
auth- |
| 6 |
Test Organisation Patterns |
MEDIUM |
pest- |
Quick Reference
1. HTTP & Feature Tests (CRITICAL)
http-test-structure - Arrange/Act/Assert with factories — Pest + PHPUnit examples
http-assert-response - assertStatus, assertJson, assertRedirect, assertJsonMissing
http-assert-json-fluent - Fluent assertJson with AssertableJson closure
http-refresh-database - RefreshDatabase vs DatabaseTransactions — when to use each
2. Model Factories (CRITICAL)
factory-define - Define factories with typed fake data and PHP 8.3 syntax
factory-states - Factory states for distinct test scenarios
factory-sequences - sequence() for varied data across multiple records
factory-relationships - has(), for(), recycle(), afterCreating()
3. Database Assertions (HIGH)
db-assert-has - assertDatabaseHas, assertModelExists for presence checks
db-assert-missing - assertDatabaseMissing, assertModelMissing for deletion
db-assert-soft-deletes - assertSoftDeleted, trashed() factory state
4. Faking Services (HIGH)
fake-mail - Mail::fake(), assertSent vs assertQueued, assertNothingSent
fake-queue - Queue::fake(), assertPushed, assertPushedOn
fake-notification - Notification::fake(), assertSentTo, assertCount
fake-event - Event::fake(), assertDispatched, assertNotDispatched
fake-storage - Storage::fake(), UploadedFile::fake(), assertExists
fake-ai-agent - Agent::fake(), assertPrompted, preventStrayPrompts (Laravel 13+)
fake-ai-media - Image::fake(), Audio::fake(), Transcription::fake() (Laravel 13+)
fake-ai-data - Embeddings::fake(), Reranking::fake(), Files::fake(), Stores::fake() (Laravel 13+)
5. Authentication Testing (HIGH)
auth-acting-as - actingAs() for session/web authenticated tests
auth-sanctum - Sanctum::actingAs() for API token authentication
6. Test Organisation Patterns (MEDIUM)
pest-describe-it - describe()/it() (Pest) or test class organisation (PHPUnit)
pest-datasets - with() datasets (Pest) or #[DataProvider] (PHPUnit)
pest-hooks - beforeEach/afterEach (Pest) or setUp/tearDown (PHPUnit)
Essential Patterns
Pest
<?php
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
test('authenticated user can create a post', function () {
$user = User::factory()->create();
$this->actingAs($user)
->postJson('/api/posts', ['title' => 'Hello World', 'body' => 'Content.'])
->assertStatus(201)
->assertJsonPath('data.title', 'Hello World');
$this->assertDatabaseHas('posts', ['title' => 'Hello World', 'user_id' => $user->id]);
});
PHPUnit
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class PostControllerTest extends TestCase
{
use RefreshDatabase;
public function test_authenticated_user_can_create_a_post(): void
{
$user = User::factory()->create();
$this->actingAs($user)
->postJson('/api/posts', ['title' => 'Hello World', 'body' => 'Content.'])
->assertStatus(201)
->assertJsonPath('data.title', 'Hello World');
$this->assertDatabaseHas('posts', ['title' => 'Hello World', 'user_id' => $user->id]);
}
}
How to Use
Read individual rule files for detailed explanations and code examples.
Each rule file contains:
- YAML frontmatter with metadata (title, impact, tags)
- Brief explanation of why it matters
- Bad Example with explanation
- Good Example with both Pest and PHPUnit where syntax differs
- Laravel 13 specific context and references
Full Compiled Document
For the complete guide with all rules expanded: AGENTS.md
1---2name: laravel-testing3description: Laravel 13 testing with Pest PHP 4 or PHPUnit 12. Use when writing feature tests, unit tests, or any test code in a Laravel application. Triggers on tasks involving HTTP tests, model factories, database assertions, mocking facades, authentication testing, or test organisation patterns.4license: MIT5---67# Laravel 13 Testing — Pest PHP 4 & PHPUnit 1289> **Supports both Pest PHP 4 and PHPUnit 12.** See Framework Detection below.10>11> **PHPUnit version note:** Laravel 13 ships with `phpunit/phpunit: ^12.5.12` in its default `composer.json`. All patterns in this skill are compatible with PHPUnit 11, 12, and 13.1213Comprehensive testing guide for Laravel 13 applications. Contains 24 rules across 6 categories for writing fast, readable, and reliable tests. Supports both **Pest PHP 4** and **PHPUnit 12** (Laravel 13 default).1415## Framework Detection1617**Before writing or reviewing any test code, detect which testing framework the project uses:**1819### Step 1 — Check composer.json2021```bash22# Look for these in require-dev:23# "pestphp/pest" → Pest24# "phpunit/phpunit" (without pest) → PHPUnit25```2627- If `pestphp/pest` is present → **use Pest syntax**28- If only `phpunit/phpunit` is present → **use PHPUnit syntax**29- If both are present → **Pest takes priority** (Pest runs on top of PHPUnit)3031### Step 2 — Check for tests/Pest.php3233- If `tests/Pest.php` exists → **Pest is configured**, use Pest syntax3435### Step 3 — If still unclear, ask the user3637> "I couldn't detect the testing framework. Does this project use **Pest PHP** or **PHPUnit**?"3839---4041## Syntax Reference4243### Test Declaration4445| | Pest | PHPUnit |46|--|------|---------|47| Test function | `test('...', fn() => ...)` | `public function test_...(): void` |48| Readable name | `it('...', fn() => ...)` | `#[Test] public function it_...()` |49| Grouping | `describe('...', fn() => ...)` | Test class name / nested classes |50| Trait application | `uses(RefreshDatabase::class)` | `use RefreshDatabase;` inside class |51| Before each | `beforeEach(fn() => ...)` | `protected function setUp(): void` |52| After each | `afterEach(fn() => ...)` | `protected function tearDown(): void` |53| Parameterised | `->with([...])` | `#[DataProvider]` attribute |54| Global setup | `uses(...)->in('Feature')` in `Pest.php` | Base `TestCase` class |5556### Core assertions (identical in both frameworks)5758`assertStatus`, `assertJson`, `assertJsonPath`, `assertDatabaseHas`, `assertModelExists`, `actingAs`, `Mail::fake()`, `Queue::fake()`, `Event::fake()`, `Notification::fake()`, `Storage::fake()` — all work the same in Pest and PHPUnit.5960---6162## When to Apply6364Reference these guidelines when:65- Writing feature or unit tests for Laravel66- Testing HTTP endpoints and API responses67- Creating factories and test data68- Asserting database state after operations69- Faking Mail, Queue, Notification, or Event facades70- Testing authenticated routes and API tokens71- Organising tests with describe blocks, datasets, or test classes7273## Rule Categories by Priority7475| Priority | Category | Impact | Prefix |76|----------|----------|--------|--------|77| 1 | HTTP & Feature Tests | CRITICAL | `http-` |78| 2 | Model Factories | CRITICAL | `factory-` |79| 3 | Database Assertions | HIGH | `db-` |80| 4 | Faking Services | HIGH | `fake-` |81| 5 | Authentication Testing | HIGH | `auth-` |82| 6 | Test Organisation Patterns | MEDIUM | `pest-` |8384## Quick Reference8586### 1. HTTP & Feature Tests (CRITICAL)8788- `http-test-structure` - Arrange/Act/Assert with factories — Pest + PHPUnit examples89- `http-assert-response` - assertStatus, assertJson, assertRedirect, assertJsonMissing90- `http-assert-json-fluent` - Fluent assertJson with AssertableJson closure91- `http-refresh-database` - RefreshDatabase vs DatabaseTransactions — when to use each9293### 2. Model Factories (CRITICAL)9495- `factory-define` - Define factories with typed fake data and PHP 8.3 syntax96- `factory-states` - Factory states for distinct test scenarios97- `factory-sequences` - sequence() for varied data across multiple records98- `factory-relationships` - has(), for(), recycle(), afterCreating()99100### 3. Database Assertions (HIGH)101102- `db-assert-has` - assertDatabaseHas, assertModelExists for presence checks103- `db-assert-missing` - assertDatabaseMissing, assertModelMissing for deletion104- `db-assert-soft-deletes` - assertSoftDeleted, trashed() factory state105106### 4. Faking Services (HIGH)107108- `fake-mail` - Mail::fake(), assertSent vs assertQueued, assertNothingSent109- `fake-queue` - Queue::fake(), assertPushed, assertPushedOn110- `fake-notification` - Notification::fake(), assertSentTo, assertCount111- `fake-event` - Event::fake(), assertDispatched, assertNotDispatched112- `fake-storage` - Storage::fake(), UploadedFile::fake(), assertExists113- `fake-ai-agent` - Agent::fake(), assertPrompted, preventStrayPrompts (Laravel 13+)114- `fake-ai-media` - Image::fake(), Audio::fake(), Transcription::fake() (Laravel 13+)115- `fake-ai-data` - Embeddings::fake(), Reranking::fake(), Files::fake(), Stores::fake() (Laravel 13+)116117### 5. Authentication Testing (HIGH)118119- `auth-acting-as` - actingAs() for session/web authenticated tests120- `auth-sanctum` - Sanctum::actingAs() for API token authentication121122### 6. Test Organisation Patterns (MEDIUM)123124- `pest-describe-it` - describe()/it() (Pest) or test class organisation (PHPUnit)125- `pest-datasets` - with() datasets (Pest) or #[DataProvider] (PHPUnit)126- `pest-hooks` - beforeEach/afterEach (Pest) or setUp/tearDown (PHPUnit)127128## Essential Patterns129130### Pest131132```php133<?php134135use App\Models\User;136use Illuminate\Foundation\Testing\RefreshDatabase;137138uses(RefreshDatabase::class);139140test('authenticated user can create a post', function () {141 $user = User::factory()->create();142143 $this->actingAs($user)144 ->postJson('/api/posts', ['title' => 'Hello World', 'body' => 'Content.'])145 ->assertStatus(201)146 ->assertJsonPath('data.title', 'Hello World');147148 $this->assertDatabaseHas('posts', ['title' => 'Hello World', 'user_id' => $user->id]);149});150```151152### PHPUnit153154```php155<?php156157namespace Tests\Feature;158159use App\Models\User;160use Illuminate\Foundation\Testing\RefreshDatabase;161use Tests\TestCase;162163class PostControllerTest extends TestCase164{165 use RefreshDatabase;166167 public function test_authenticated_user_can_create_a_post(): void168 {169 $user = User::factory()->create();170171 $this->actingAs($user)172 ->postJson('/api/posts', ['title' => 'Hello World', 'body' => 'Content.'])173 ->assertStatus(201)174 ->assertJsonPath('data.title', 'Hello World');175176 $this->assertDatabaseHas('posts', ['title' => 'Hello World', 'user_id' => $user->id]);177 }178}179```180181## How to Use182183Read individual rule files for detailed explanations and code examples.184185Each rule file contains:186- YAML frontmatter with metadata (title, impact, tags)187- Brief explanation of why it matters188- Bad Example with explanation189- Good Example with both Pest and PHPUnit where syntax differs190- Laravel 13 specific context and references191192## Full Compiled Document193194For the complete guide with all rules expanded: `AGENTS.md`