generated from theradcoza/Laravel-Docker-Dev-Template
387 lines
7.9 KiB
Markdown
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
|