# 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 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