laravel-testing
Laravel 12 Testing — Pest PHP 4 & PHPUnit 11
Supports both Pest PHP 4 and PHPUnit 11. See Framework Detection below.
PHPUnit version note: Laravel 12 ships with
phpunit/phpunit: ^11.5.50in its defaultcomposer.json. PHPUnit 12 and 13 exist but are not the Laravel 12 default — to use them, manually update the constraint. All patterns in this skill are compatible with PHPUnit 11, 12, and 13.
Comprehensive testing guide for Laravel 12 applications. Contains 21 rules across 6 categories for writing fast, readable, and reliable tests. Supports both Pest PHP 4 and PHPUnit 11 (Laravel 12 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/pestis present → use Pest syntax - If only
phpunit/phpunitis 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.phpexists → 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 method |
| 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 exampleshttp-assert-response- assertStatus, assertJson, assertRedirect, assertJsonMissinghttp-assert-json-fluent- Fluent assertJson with AssertableJson closurehttp-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 syntaxfactory-states- Factory states for distinct test scenariosfactory-sequences- sequence() for varied data across multiple recordsfactory-relationships- has(), for(), recycle(), afterCreating()
3. Database Assertions (HIGH)
db-assert-has- assertDatabaseHas, assertModelExists for presence checksdb-assert-missing- assertDatabaseMissing, assertModelMissing for deletiondb-assert-soft-deletes- assertSoftDeleted, trashed() factory state
4. Faking Services (HIGH)
fake-mail- Mail::fake(), assertSent vs assertQueued, assertNothingSentfake-queue- Queue::fake(), assertPushed, assertPushedOnfake-notification- Notification::fake(), assertSentTo, assertCountfake-event- Event::fake(), assertDispatched, assertNotDispatchedfake-storage- Storage::fake(), UploadedFile::fake(), assertExists
5. Authentication Testing (HIGH)
auth-acting-as- actingAs() for session/web authenticated testsauth-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 12 specific context and references
Full Compiled Document
For the complete guide with all rules expanded: AGENTS.md