Skip to main content

Introduction

You can test your importer’s row processing using its test() method, and test submissions from an import action using ImportAction::fake().

Testing a row

Call test() 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 a columnMap to use different CSV headers, and options to test your import options:
An explicit map replaces the default entirely; omitted columns remain unmapped. Options control only the behavior you implement in your importer. The default map includes every declared column, not just the keys present in the row. To test a file that omits a column, pass an explicit map without that column. Omitting a row value alone does not remove the column’s validation rules. 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 unsaved Import 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

Use assertSkipped() when your importer’s resolveRecord() returns null. For example, if your importer returns null for products that do not exist:
A skipped row completes without an exception and has no record. It is not a validation error or a deliberate row failure.

Asserting validation errors

Use assertHasErrors() and assertHasNoErrors() to check validation errors, including those raised by lifecycle hooks:
Without arguments, these methods check for any validation errors or none. You can pass a field list such as ['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

Use assertHasRowFailure() to check for a RowImportFailedException. For example, the updates-only importer can throw when no matching product exists:
Omit the message to check for any deliberate row failure, or pass a message to check an exact match. Use 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 a PostImporter that resolves authors by email:
Also test your resolver’s behavior for missing relationships, whether it rejects the row or creates a related record. For other hook side effects, use Laravel’s fakes or database assertions, such as checking the record ID passed to a dispatched job.

Testing import action submissions

Use ImportAction::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:
Upload the file before setting 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:
For example, if 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.