NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages

Imports::fake() swaps the manager for an ImportsFake behind the facade and in the container, and returns it:

use Illuminate\Support\Facades\Bus;
use RoundlyConsulting\Imports\Facades\Imports;
use RoundlyConsulting\Imports\Models\ImportSession;
use RoundlyConsulting\Imports\Testing\SpreadsheetFactory;

it('imports the uploaded assets', function () {
    $fake = Imports::fake();   // records every operation and passes it through
    Bus::fake();               // optional: keep commits from running

    $this->actingAs($admin)->post('/assets/import', ['file' => SpreadsheetFactory::csv(['Asset tag', 'Name'], [['AST-1', 'Laptop']])]);

    Imports::assertUploaded('assets', fn (ImportSession $session) => $session->owner->is($team));
    Imports::assertPreviewed();
    Imports::assertNothingCommitted();
    expect($fake->recorded('upload'))->toHaveCount(1);
});
  • It records every operation and passes it through: handles keep returning real sessions and batches. Add Bus::fake() to keep commits from running.
  • It records calls made through the facade, an injected manager (ImportsFake extends ImportsManager), every handle and the model conveniences ($session->handle(), $batch->handle()).
  • Reads — status(), counts(), rows(), suggestMapping(), … — are not recorded.
  • ->withoutRunning() records without running the operations that return nothing a test needs — cancel, discard, resume, rollback, mapping deletes, prune and sweep (they return null or an empty report). mappings(…)->delete() returns true, as for an existing mapping, so the delete-mapping endpoint answers 200; the recorded result stays null. Everything else still runs.
use RoundlyConsulting\Imports\Models\ImportBatch;

$fake = Imports::fake()->withoutRunning();   // cancel, discard, resume, rollback, deleteMapping, prune, sweep: record only

Imports::for($team)->by($admin)->batch($batchId)->rollback();   // recorded, not run

Imports::assertRolledBack(fn (ImportBatch $batch) => $batch->getKey() === $batchId);
$fake->recorded('rollback')[0]->action;     // the action class it would have run

Assertions

Every assertion has a negative pair and is listed on the facade, so Imports::assertModeChosen() completes in an IDE after Imports::fake(). A callback receives the model and returns bool. assertMappingSaved() covers session saves and mappings()->save() alike:

AssertionNegativeCallback receives
assertUploaded(?string $importer = null, ?Closure $callback = null)assertNothingUploaded()ImportSession
assertFilesUploaded(?string $importer = null, ?Closure $callback = null)assertNoFilesUploaded()ImportSession
assertSourceConfigured(?Closure $callback = null)assertNoSourceConfigured()ImportSession
assertMapped(?Closure $callback = null)assertNothingMapped()ImportSession
assertMappingSaved(?string $name = null)assertNoMappingSaved()—
assertMappingDeleted(?string $name = null)assertNoMappingDeleted()—
assertModeChosen(?DuplicateMode $mode = null, ?Closure $callback = null)assertNoModeChosen()ImportSession
assertPreviewed(?Closure $callback = null)assertNothingPreviewed()ImportSession
assertCommitted(?string $importer = null, ?Closure $callback = null)assertNothingCommitted()ImportBatch
assertCancelled(?Closure $callback = null)assertNothingCancelled()ImportBatch
assertResumed(?Closure $callback = null)assertNothingResumed()ImportBatch
assertRolledBack(?Closure $callback = null)assertNothingRolledBack()ImportBatch
assertDiscarded(?Closure $callback = null)assertNothingDiscarded()ImportSession
assertTemplateBuilt(?string $importer = null, ?TemplateFormat $format = null, ?string $locale = null)assertNoTemplateBuilt()—
assertErrorReportBuilt(?Closure $callback = null)assertNoErrorReportBuilt()ImportSession
assertPruned()assertNotPruned()—
assertSwept()assertNotSwept()—
assertNothingHappened()——

Recorded operations

recorded(?string $operation = null) returns the RecordedOperation list — each with operation, context (the session, batch, importer, owner, mode, format, … it was called with), result and action (the action class that ran it). Operation names: upload, uploadFiles, configureSource, map, saveMapping, deleteMapping, mode, preview, template, errorReport, commit, cancel, resume, rollback, discard, prune, sweep.

Building files

SpreadsheetFactory builds CSV, XLSX and ZIP uploads — each returns an UploadedFile. An empty header list writes no header row, XLSX rows also take DateTimeInterface values, and encoding also takes UTF-16LE, UTF-16BE, Windows-1252 and Windows-1250 to test the rejections:

use RoundlyConsulting\Imports\Testing\SpreadsheetFactory;

SpreadsheetFactory::csv(['Asset tag', 'Name'], [['AST-1', 'Laptop']], delimiter: ';', bom: true, encoding: 'UTF-8', eol: "\n", name: 'import.csv');
SpreadsheetFactory::xlsx(['Asset tag', 'Name'], [['AST-1', 'Laptop']], sheet: 'Assets', extraSheets: ['Other' => [['A']]], date1904: false, sharedStrings: true, name: 'import.xlsx');
SpreadsheetFactory::zip(['a.pdf' => '%PDF', 'b.pdf' => '%PDF'], name: 'files.zip');
SpreadsheetFactory::file('notes.txt', 'hello', mime: 'text/plain');

SpreadsheetFactory::csv(['Name'], [['Žltý kôň']], encoding: 'Windows-1250');   // to test the not_utf8 rejection

Factories

ImportSession, ImportBatch, ImportRowResult and ImportedRecord all ship factories:

use RoundlyConsulting\Imports\Enums\SessionStatus;
use RoundlyConsulting\Imports\Models\{ImportedRecord, ImportSession};

ImportSession::factory()->owner($team)->status(SessionStatus::Previewed)->create();
ImportedRecord::factory()->inBatch($batch)->record($asset)->create();

Queued previews and commits

// Run queued previews and commits in a test
Artisan::call('queue:work', ['--stop-when-empty' => true]);

// or run everything inline
config(['imports.queue.enabled' => false]);

Try it locally

The package’s repository ships a workbench app for manual review: two teams, each with an admin who may import and a member who may not, vendors to reference, three importers (assets, people, documents) and the JSON API on:

vendor/bin/testbench workbench:build      # a fresh SQLite database, every migration, the seeder
vendor/bin/testbench serve                # http://127.0.0.1:8000
vendor/bin/testbench queue:work           # in a second terminal: large previews, commits and undo

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.