| name | pest-testing |
| description | Pest PHP testing patterns for this monorepo. Use when writing tests for Laravel controllers, services, models, or authentication flows. Triggers on tasks involving Pest, PHPUnit, feature tests, unit tests, HTTP assertions, or test factories. |
| frameworks | ["laravel","pest"] |
| languages | ["php"] |
| category | testing |
| updated | "2026-04-29T00:00:00.000Z" |
Pest Testing Skill
Quick Reference
When to Use: Writing tests for any PHP code in apps/laravel/
Test runner: Pest 3 (wraps PHPUnit), run via pnpm -F @repo/laravel test
Test types:
tests/Feature/ — HTTP-level integration tests (hit routes, DB, auth)
tests/Unit/ — Pure unit tests (models, helpers, no HTTP)
Running Tests
pnpm -F @repo/laravel test
docker compose exec api php artisan test tests/Feature/Auth/LoginTest.php
docker compose exec api php artisan test --filter="user can login"
docker compose exec api php artisan test --coverage
Feature Test Pattern
use App\Models\Todo;
use App\Models\User;
uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);
it('lists todos for the authenticated user', function () {
$user = User::factory()->create();
Todo::factory(3)->create(['user_id' => $user->id]);
Todo::factory(2)->create();
$response = $this->actingAs($user)
->getJson('/api/v1/todos');
$response->assertOk()
->assertJsonCount(3);
});
it('creates a todo', function () {
$user = User::factory()->create();
$response = $this->actingAs($user)
->postJson('/api/v1/todos', ['title' => 'Buy milk']);
$response->assertCreated()
->assertJsonPath('title', 'Buy milk')
->assertJsonPath('completed', false);
$this->assertDatabaseHas('todos', [
'title' => 'Buy milk',
'user_id' => $user->id,
]);
});
it('prevents unauthenticated access', function () {
$this->getJson('/api/v1/todos')->assertUnauthorized();
});
Auth Test Pattern
uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);
it('returns a PAT on successful login', function () {
$user = User::factory()->create(['password' => bcrypt('secret123')]);
$response = $this->postJson('/api/v1/auth/mobile/login', [
'email' => $user->email,
'password' => 'secret123',
]);
$response->assertOk()
->assertJsonStructure(['token', 'token_type', 'user']);
expect($response->json('token_type'))->toBe('Bearer');
});
it('rejects invalid credentials', function () {
User::factory()->create(['email' => 'test@example.com']);
$this->postJson('/api/v1/auth/mobile/login', [
'email' => 'test@example.com',
'password' => 'wrong-password',
])->assertUnprocessable();
});
Unit Test Pattern
use App\Models\Todo;
use App\Models\User;
uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);
it('belongs to a user', function () {
$todo = Todo::factory()->create();
expect($todo->user)->toBeInstanceOf(User::class);
});
it('has correct casts', function () {
$todo = Todo::factory()->create(['completed' => true]);
expect($todo->completed)->toBeBool()->toBeTrue();
});
Assertion Reference
$response->assertOk();
$response->assertCreated();
$response->assertNoContent();
$response->assertUnauthorized();
$response->assertForbidden();
$response->assertNotFound();
$response->assertUnprocessable();
$response->assertJson(['key' => 'value']);
$response->assertJsonPath('user.email', 'test@example.com');
$response->assertJsonCount(3);
$response->assertJsonStructure(['id', 'title', 'user' => ['id', 'name']]);
$response->assertJsonMissing(['password']);
$this->assertDatabaseHas('todos', ['title' => 'Buy milk']);
$this->assertDatabaseMissing('todos', ['id' => $id]);
$this->assertDatabaseCount('todos', 5);
Pest-Specific Assertions (expect API)
expect($value)->toBe(42);
expect($user)->toBeInstanceOf(User::class);
expect($collection)->toHaveCount(3);
expect($string)->toContain('substring');
expect($array)->toMatchArray(['key' => 'value']);
expect($nullable)->toBeNull();
expect($bool)->toBeTrue();
expect(fn () => riskyOperation())->toThrow(Exception::class);
Test Setup Helpers
beforeEach(function () {
$this->user = User::factory()->create();
});
beforeEach(function () {
$this->actingAs(User::factory()->create());
});
it('skips this test', function () { ... })->skip('reason');
it('is todo', function () { ... })->todo();
Best Practices
- Use
RefreshDatabase for tests that touch the DB — it wraps each test in a transaction
- Use factories (
User::factory()->create()) rather than raw DB inserts
- Test the HTTP contract (status, response shape), not internal implementation
- One assertion per logical concern — keep tests small and focused
- Prefer
actingAs($user) over manually setting Authorization headers
Adding a New Feature Test Checklist
- Create
tests/Feature/[FeatureName]Test.php
uses(RefreshDatabase::class) at the top
- Write test for the happy path (authenticated, valid input)
- Write test for 401 (unauthenticated)
- Write test for 422 (invalid input)
- Write test for 404 (not found / wrong owner)
- Run
pnpm -F @repo/laravel test