generated from theradcoza/Laravel-Docker-Dev-Template
Initial commit
This commit is contained in:
386
docs/testing.md
Normal file
386
docs/testing.md
Normal file
@@ -0,0 +1,386 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user