Files
Laravel-Docker-Dev-Template/docs/testing.md
2026-03-06 08:57:05 +02:00

387 lines
7.9 KiB
Markdown

# Testing with Pest
This template uses [Pest](https://pestphp.com/) for testing - a modern PHP testing framework with elegant syntax.
## Quick Start
```bash
# Run all tests
make test
# Run with coverage
make test-coverage
# Run specific module tests
make test-module module=Inventory
# Run filtered tests
make test-filter filter="can create"
# Run in parallel (faster)
make test-parallel
```
## Test Structure
```
tests/
├── Feature/ # HTTP/integration tests
│ └── ExampleTest.php
├── Unit/ # Isolated unit tests
│ └── ExampleTest.php
├── Modules/ # Module-specific tests
│ └── Inventory/
│ ├── InventoryTest.php
│ └── ProductTest.php
├── Pest.php # Global configuration
├── TestCase.php # Base test class
└── TestHelpers.php # Custom helpers
```
## Writing Tests
### Basic Syntax
```php
// Simple test
it('has a welcome page', function () {
$this->get('/')->assertStatus(200);
});
// With description
test('users can login', function () {
$user = User::factory()->create();
$this->post('/login', [
'email' => $user->email,
'password' => 'password',
])->assertRedirect('/dashboard');
});
```
### Expectations
```php
it('can create a product', function () {
$product = Product::create(['name' => 'Widget', 'price' => 99.99]);
expect($product)
->toBeInstanceOf(Product::class)
->name->toBe('Widget')
->price->toBe(99.99);
});
```
### Grouped Tests
```php
describe('Product Model', function () {
it('can be created', function () {
// ...
});
it('has a price', function () {
// ...
});
it('belongs to a category', function () {
// ...
});
});
```
### Setup/Teardown
```php
beforeEach(function () {
$this->user = User::factory()->create();
});
afterEach(function () {
// Cleanup if needed
});
it('has a user', function () {
expect($this->user)->toBeInstanceOf(User::class);
});
```
## Global Helpers
Defined in `tests/Pest.php`:
```php
// Create a regular user
$user = createUser();
$user = createUser(['name' => 'John']);
// Create an admin user
$admin = createAdmin();
```
## Module Testing
When you create a module with `php artisan make:module`, tests are auto-generated:
```
tests/Modules/Inventory/
├── InventoryTest.php # Route and permission tests
└── ProductTest.php # Model tests (if --model used)
```
### Example Module Test
```php
<?php
use App\Models\User;
use App\Modules\Inventory\Models\Product;
beforeEach(function () {
$this->user = User::factory()->create();
$this->user->givePermissionTo([
'inventory.view',
'inventory.create',
]);
});
describe('Inventory Module', function () {
it('allows authenticated users to view page', function () {
$this->actingAs($this->user)
->get('/inventory')
->assertStatus(200);
});
it('redirects guests to login', function () {
$this->get('/inventory')
->assertRedirect('/login');
});
});
describe('Product Model', function () {
it('can be created', function () {
$product = Product::create([
'name' => 'Test Product',
'price' => 29.99,
]);
expect($product->name)->toBe('Test Product');
});
it('is audited on create', function () {
$this->actingAs($this->user);
$product = Product::create(['name' => 'Audited']);
$this->assertDatabaseHas('audits', [
'auditable_type' => Product::class,
'auditable_id' => $product->id,
'event' => 'created',
]);
});
});
```
## Testing Helpers
Defined in `tests/TestHelpers.php`:
```php
use Tests\TestHelpers;
uses(TestHelpers::class)->in('Modules');
// In your test
it('allows users with permission', function () {
$user = $this->userWithPermissions(['inventory.view']);
$this->actingAs($user)
->get('/inventory')
->assertStatus(200);
});
it('allows module access', function () {
$user = $this->userWithModuleAccess('inventory');
// User has view, create, edit, delete permissions
});
it('audits changes', function () {
$product = Product::create(['name' => 'Test']);
$this->assertAudited($product, 'created');
});
```
## Testing Livewire Components
```php
use Livewire\Livewire;
use App\Livewire\CreateProduct;
it('can create product via Livewire', function () {
$this->actingAs(createUser());
Livewire::test(CreateProduct::class)
->set('name', 'New Product')
->set('price', 49.99)
->call('save')
->assertHasNoErrors()
->assertRedirect('/products');
$this->assertDatabaseHas('products', [
'name' => 'New Product',
]);
});
it('validates required fields', function () {
Livewire::test(CreateProduct::class)
->set('name', '')
->call('save')
->assertHasErrors(['name' => 'required']);
});
```
## Testing Filament Resources
```php
use App\Modules\Inventory\Models\Product;
use function Pest\Livewire\livewire;
it('can list products in admin', function () {
$user = createAdmin();
$products = Product::factory()->count(5)->create();
$this->actingAs($user)
->get('/admin/inventory/products')
->assertSuccessful()
->assertSeeText($products->first()->name);
});
it('can create product from admin', function () {
$user = createAdmin();
livewire(\App\Modules\Inventory\Filament\Resources\ProductResource\Pages\CreateProduct::class)
->fillForm([
'name' => 'Admin Product',
'price' => 100,
])
->call('create')
->assertHasNoFormErrors();
$this->assertDatabaseHas('products', [
'name' => 'Admin Product',
]);
});
```
## Database Testing
Tests use SQLite in-memory for speed. The `LazilyRefreshDatabase` trait only runs migrations when needed.
### Factories
```php
// Create model
$product = Product::factory()->create();
// Create multiple
$products = Product::factory()->count(5)->create();
// With attributes
$product = Product::factory()->create([
'name' => 'Special Product',
'price' => 999.99,
]);
// With state
$product = Product::factory()->active()->create();
```
### Assertions
```php
// Database has record
$this->assertDatabaseHas('products', [
'name' => 'Widget',
]);
// Database missing record
$this->assertDatabaseMissing('products', [
'name' => 'Deleted Product',
]);
// Count records
$this->assertDatabaseCount('products', 5);
```
## HTTP Testing
```php
// GET request
$this->get('/products')->assertStatus(200);
// POST with data
$this->post('/products', [
'name' => 'New Product',
])->assertRedirect('/products');
// JSON API
$this->getJson('/api/products')
->assertStatus(200)
->assertJsonCount(5, 'data');
// With auth
$this->actingAs($user)
->get('/admin')
->assertStatus(200);
// Assert view
$this->get('/products')
->assertViewIs('products.index')
->assertViewHas('products');
```
## Running Specific Tests
```bash
# Single file
php artisan test tests/Feature/ExampleTest.php
# Single test by name
php artisan test --filter="can create product"
# Module tests only
php artisan test tests/Modules/Inventory
# With verbose output
php artisan test --verbose
# Stop on first failure
php artisan test --stop-on-failure
```
## Coverage
```bash
# Generate coverage report
make test-coverage
# Requires Xdebug or PCOV
# Coverage report saved to coverage/
```
## Tips
1. **Use factories** - Don't manually create models in tests
2. **Test behavior, not implementation** - Focus on what, not how
3. **One assertion per test** - Keep tests focused
4. **Use descriptive names** - `it('shows error when email is invalid')`
5. **Test edge cases** - Empty values, boundaries, errors
6. **Run tests often** - Before commits, after changes