Why Testing Filament Deserves Its Own Strategy
Filament ships a first-class testing API built on top of Livewire's testing utilities. Most teams either skip it entirely or write brittle browser tests. Neither is acceptable when a resource manages production data. The goal here is fast, deterministic Pest tests that cover the paths users actually hit.
Project Setup
Install the Filament testing package alongside Pest:
composer require --dev filament/filament pestphp/pest-plugin-laravel
Filament's helpers live in Filament\Tests and are pulled in automatically when you use livewire() inside a test. No extra trait is needed beyond RefreshDatabase.
Testing a List Page
The list page is the most common entry point. Assert that the table renders and that records appear:
use App\Filament\Resources\OrderResource;
use App\Models\Order;
it('renders the order list page', function () {
$this->actingAs(User::factory()->admin()->create());
$orders = Order::factory(3)->create();
livewire(OrderResource\Pages\ListOrders::class)
->assertCanSeeTableRecords($orders);
});
assertCanSeeTableRecords checks that the Livewire component's rendered HTML contains each model's row — it does not fire a real browser.
Filtering and Searching
it('filters orders by status', function () {
$pending = Order::factory()->pending()->create();
$shipped = Order::factory()->shipped()->create();
livewire(OrderResource\Pages\ListOrders::class)
->filterTable('status', 'pending')
->assertCanSeeTableRecords([$pending])
->assertCanNotSeeTableRecords([$shipped]);
});
The filterTable($filterName, $value) helper maps directly to the filter key you registered on the table.
Testing Table Actions
Bulk actions and row actions are where bugs hide. Test them explicitly:
it('marks an order as shipped via row action', function () {
$order = Order::factory()->pending()->create();
livewire(OrderResource\Pages\ListOrders::class)
->callTableAction('mark_shipped', $order)
->assertHasNoTableActionErrors();
expect($order->fresh()->status)->toBe('shipped');
});
For actions that open a modal with a form, fill the fields before calling:
it('assigns a courier via action modal', function () {
$order = Order::factory()->create();
$courier = Courier::factory()->create();
livewire(OrderResource\Pages\ListOrders::class)
->mountTableAction('assign_courier', $order)
->setTableActionData(['courier_id' => $courier->id])
->callMountedTableAction()
->assertHasNoTableActionErrors();
expect($order->fresh()->courier_id)->toBe($courier->id);
});
Testing Create and Edit Forms
Form assertions validate that Filament's schema maps correctly to your model:
it('creates an order with valid data', function () {
livewire(OrderResource\Pages\CreateOrder::class)
->fillForm([
'customer_id' => Customer::factory()->create()->id,
'notes' => 'Rush delivery',
'status' => 'pending',
])
->call('create')
->assertHasNoFormErrors();
$this->assertDatabaseHas('orders', ['notes' => 'Rush delivery']);
});
it('fails validation when customer is missing', function () {
livewire(OrderResource\Pages\CreateOrder::class)
->fillForm(['notes' => 'No customer'])
->call('create')
->assertHasFormErrors(['customer_id' => 'required']);
});
Edit Page — Pre-filled State
it('pre-fills the edit form with existing data', function () {
$order = Order::factory()->create(['notes' => 'Original note']);
livewire(OrderResource\Pages\EditOrder::class, ['record' => $order->getRouteKey()])
->assertFormSet(['notes' => 'Original note']);
});
assertFormSet is the quickest way to verify that fill() inside mount() works correctly.
Authorization Inside Tests
Don't forget to test that unauthorized users are blocked:
it('prevents non-admins from deleting orders', function () {
$this->actingAs(User::factory()->create()); // no admin role
$order = Order::factory()->create();
livewire(OrderResource\Pages\ListOrders::class)
->assertTableActionHidden('delete', $order);
});
assertTableActionHidden confirms the action is not rendered for that record — no need to call it and catch an exception.
Key Takeaways
- Use
livewire(PageClass::class)— Filament pages are Livewire components and test identically. filterTable,callTableAction, andfillFormcover 90% of real user paths.- Always assert the database state after mutations, not just the absence of errors.
- Test authorization with
assertTableActionHidden/assertTableActionVisiblerather than role-checking in isolation. - Keep each test focused on one behavior; share fixture setup via Pest
beforeEachor dataset factories.