writing-feature-tests
Write feature tests for HTTP endpoints, controllers, and full request/response cycles using Pest v4. Use when ANY business logic is added to codebase. Feature tests are the PRIMARY testing approach - use for all Controllers, Actions, Form Requests, and application behavior. This is your default testing strategy.
When & Why to Use This Skill
This Claude skill automates the generation of high-quality feature tests for Laravel applications using Pest v4. It streamlines the testing of HTTP endpoints, controllers, and full request/response cycles, ensuring business logic is robust, type-safe, and follows the Arrange-Act-Assert (AAA) pattern for maximum reliability and SEO-friendly code maintainability.
Use Cases
- Developing new HTTP endpoints and automatically generating corresponding feature tests to validate status codes, redirects, and JSON responses.
- Ensuring data integrity by writing tests that verify database state changes (assertDatabaseHas) after complex form submissions or API calls.
- Maintaining high code quality standards by enforcing strict type-hinting and PHPStan level 8 compliance within the automated test suite.
- Rapidly creating comprehensive test coverage for authentication flows, including user registration, login, and protected route access using actingAs helpers.
- Refactoring legacy business logic with confidence by establishing a baseline of feature tests that cover the entire request/response lifecycle.
| name | writing-feature-tests |
|---|---|
| description | Write feature tests for HTTP endpoints, controllers, and full request/response cycles using Pest v4. Use when ANY business logic is added to codebase. Feature tests are the PRIMARY testing approach - use for all Controllers, Actions, Form Requests, and application behavior. This is your default testing strategy. |
Write Feature Tests
Feature tests are your primary testing approach for all business logic. Write feature tests for ALL logic added to the app, testing the full HTTP request/response cycle. They are fast, comprehensive, and should always be your default starting point.
Testing Hierarchy
- Feature Tests (PRIMARY) - Default for all business logic and HTTP endpoints (you're here)
- Unit Tests (SUPPLEMENTARY) - Sprinkle in for complex, concentrated areas
- Browser Tests (MINIMAL) - Only for core areas and JS interactions
File Structure
tests/Feature/Http/Controllers/{ControllerName}/{MethodName}Test.php
Examples:
tests/Feature/Http/Controllers/Auth/RegisteredUserController/StoreTest.phptests/Feature/Http/Controllers/User/ProfileController/UpdateTest.php
Core Conventions
Test Structure
<?php
declare(strict_types=1);
use App\Models\User;
test('descriptive test name in sentence case', function (): void {
// Arrange
$user = User::factory()->create();
// Act
$response = $this->actingAs($user)->post(route('endpoint'), getData());
// Assert
$response->assertValid()->assertRedirect();
expect($user->fresh())
->property->toBe('expected value');
});
// Helper functions at bottom of file
function getData(array $overrides = []): array
{
return array_merge([
'field' => 'value',
], $overrides);
}
Key Requirements:
- Always include
declare(strict_types=1); - Use
test('description', function (): void {})syntax (Pest v4) - Follow Arrange-Act-Assert (AAA) pattern
- Helper functions at bottom of file
HTTP Methods
$this->get(route('users.index'))
$this->post(route('users.store'), getData())
$this->put(route('users.update', $user), getData())
$this->delete(route('users.destroy', $user))
Authentication
$this->actingAs($user)->get(route('dashboard'))
$this->assertAuthenticated()
$this->assertGuest()
Response Assertions
->assertOk()
->assertCreated()
->assertRedirect(route('dashboard'))
->assertValid()
->assertInvalid(['email'])
->assertJson(['key' => 'value'])
Database Assertions
$this->assertDatabaseHas('users', ['email' => 'test@example.com'])
$this->assertDatabaseMissing('users', ['email' => 'deleted@example.com'])
$this->assertDatabaseCount('users', 5)
Expect Assertions
$user = User::query()->sole();
expect($user)
->name->toBe('Test User')
->email->toBe('test@example.com')
->and($user->posts)->toHaveCount(3);
Anti-Patterns
Don't Do This
// Don't skip return type
test('users can register', function () {});
// Don't hardcode URLs
$this->get('/dashboard');
// Don't create test data inline
$this->post(route('register'), [
'name' => 'Test User',
'email' => 'test@example.com',
]);
Do This Instead
// Always type hint return type
test('users can register', function (): void {});
// Use named routes
$this->get(route('dashboard.index'));
// Use helper functions
function getData(array $overrides = []): array
{
return array_merge([
'name' => 'Test User',
'email' => 'test@example.com',
], $overrides);
}
Running Tests
php artisan test --testsuite=Feature
php artisan test tests/Feature/Http/Controllers/Auth/RegisteredUserController/StoreTest.php
php artisan test --coverage --min=90
php artisan test --filter="new users can register"
Quality Standards
- All feature tests must pass PHPStan level 8
- 100% type coverage required
- 90% overall test coverage required
- Tests must be independent (no test order dependency)
- Use
LazilyRefreshDatabasetrait (configured in Pest.php) - Every controller method must have at least one feature test
- Critical paths must have multiple test cases (happy path + edge cases)
References
- references/examples.md - Complete working examples