Introduction
You can test your importer’s row processing using itstest() method, and test submissions from an import action using ImportAction::fake().
Testing a row
Calltest() on your importer, such as PostImporter::test(), to get a Filament\Actions\Testing\TestableImport instance. It runs your application’s importer, including column mapping, casting, validation, lifecycle hooks, and saving records and relationships, without uploading a CSV or dispatching import jobs:
assertImported() checks that the importer completed without an exception and resolved a record. It does not guarantee database persistence, since your importer may customize saveRecord(). Use getRecord() with model or database assertions to check the saved values.
The helper runs your importer’s database writes and other side effects without rolling them back, even when a row fails. Use your normal database isolation for tests. Test queued processing and completion notifications separately.
Passing column mappings and options
By default, each importer column is mapped to a row key with the same name. Pass acolumnMap to use different CSV headers, and options to test your import options:
TestableImport does not validate the column mapping or options forms, or apply options-form defaults. For example, requiredMapping() is enforced by the import action’s form, not this helper. Test those requirements through the import action.
Providing import context
The helper creates an unsavedImport model by default. If your importer needs a particular import or associated user, pass your own model to ProductImporter::test(import: $import). Associate its user using $import->user()->associate($user) and authenticate explicitly using $this->actingAs($user) when needed. The helper does not associate a user or change authentication for you.
If your importer uses the import ID, for example to scope cached values shared across rows, give your supplied model a key appropriate to your application or persist it when a database record is required. The helper does not generate an ID. Use the same import ID for rows from one import and different IDs when testing isolation between imports.
Asserting skipped rows
UseassertSkipped() when your importer’s resolveRecord() returns null. For example, if your importer returns null for products that do not exist:
Asserting validation errors
UseassertHasErrors() and assertHasNoErrors() to check validation errors, including those raised by lifecycle hooks:
['price', 'name'], or a field-to-rule map such as ['price' => ['numeric', 'min']]. Only the specified fields or rules are checked. Use rule names without parameters, such as 'min', since rule parameters are not compared.
For column validation, use importer column names, not CSV headers or display labels. For example, assert 'price' even when it is mapped to 'Unit price'.
Exceptions created with ValidationException::withMessages() have no failed-rule metadata. Assert their field keys or exact messages. To check that a field has no errors, use assertHasNoErrors(['custom']), since a rule-qualified negative assertion may pass despite a message on that field.
Asserting deliberate row failures
UseassertHasRowFailure() to check for a RowImportFailedException. For example, the updates-only importer can throw when no matching product exists:
assertHasNoRowFailure() to check that none occurred.
Validation errors and deliberate row failures are separate: assertHasNoErrors() only checks validation, and assertHasNoRowFailure() only checks deliberate failures. Neither establishes an imported outcome; use assertImported() for that. Unexpected exceptions propagate to your test.
Testing relationships
Use model assertions to check saved relationships. For example, for aPostImporter that resolves authors by email:
Testing import action submissions
UseImportAction::fake() with the action testing helpers to check that your action requests an import without processing its rows. For a page with an import action using ProductImporter and its updateExisting option:
columnMap, so the form can read the headers and build its mapping fields. fillForm() preserves other form defaults. The callback receives the Import model, column map, and options merged from the action and form.
assertDispatched() checks for at least one request for the importer, optionally matching a callback. assertDispatchedTimes() checks its exact count, defaulting to one. Use assertNothingDispatched() after invalid form data or rejected authorization.
Use assertNotDispatched(ProductImporter::class) to check that a particular importer was not dispatched, while allowing other importers. It accepts the same optional callback as assertDispatched() and fails if any import matches:
sku uses requiredMapping(), submit an otherwise valid form with columnMap.sku set to null, assert assertHasFormErrors(['columnMap.sku' => 'required']), then $imports->assertNothingDispatched(). These are form errors, not row validation errors.
To test submission-time authorization, mount and fill a valid form while authorized, revoke permission, and invoke ->call('callMountedAction') before asserting that nothing was dispatched. Visibility checks alone do not prove that submission is rejected.
The fake still reads the file, validates the form, and persists an Import record. It does not run import jobs, process rows, emit ImportStarted or ImportCompleted, or send completion notifications. It does not globally fake Laravel’s bus or events: unrelated jobs and events, and your action hooks, still run. Use your importer’s test() method separately to test row behavior.
The fake only intercepts dispatch through Filament’s ImportDispatcher. If your custom action dispatches jobs directly instead, use Laravel’s bus or queue fakes to test that workflow. Keep separate tests for completion listeners and unfaked integration tests for worker processing and failed-row downloads.