›
byrcsc/laravel-data-sync · 1.x
Test sync definitions with Laravel storage, queue, event, and database fakes.
A definition is best tested the way it runs: put a real file on a faked disk and process it in-process. Everything below uses Pest syntax and Laravel's own fakes; nothing package-specific is required.
use Illuminate\Support\Facades\Storage;
beforeEach(function (): void {
Storage::fake('incoming');
Storage::fake('staging');
Storage::fake('archive');
config()->set('data-sync.staging.disk', 'staging');
config()->set('data-sync.archive.disk', 'archive');
config()->set('data-sync.syncs', [App\Syncs\ProductsSync::class]);
});Fake the source, staging, and archive disks and point the package's staging and archive config at the fakes. Registering the definition through config in the test, rather than relying on the application's own list, keeps each test independent.
use App\Models\Product;
use ByRcsc\LaravelDataSync\Enums\RunStatus;
use ByRcsc\LaravelDataSync\Models\SyncRun;
it('imports products from a supplier csv', function (): void {
Storage::disk('incoming')->put('products/today.csv', <<<'CSV'
SKU,Description,Price
DS-1001,Anglepoise desk lamp,89.00
DS-1002,Mechanical keyboard,149.50
CSV);
$this->artisan('sync:run products --now')->assertSuccessful();
$run = SyncRun::query()->sole();
expect($run->status)->toBe(RunStatus::Completed)
->and($run->rows_created)->toBe(2)
->and(Product::query()->count())->toBe(2)
->and(Product::query()->where('sku', 'DS-1001')->value('price_cents'))->toBe(8900);
});--now keeps everything in the test process, which is what you want for
assertions about rows. Calling the manager directly works the same way and gives
you the report object:
use ByRcsc\LaravelDataSync\Facades\DataSync;
$report = DataSync::run('products');
expect($report->discovered)->toBe(1)
->and($report->runs[0]->created)->toBe(2);use ByRcsc\LaravelDataSync\Models\SyncRowFailure;
it('records a row that fails validation and keeps going', function (): void {
Storage::disk('incoming')->put('products/today.csv', <<<'CSV'
SKU,Description,Price
DS-1001,Anglepoise desk lamp,89.00
,Mechanical keyboard,149.50
CSV);
DataSync::run('products');
$run = SyncRun::query()->sole();
$failure = SyncRowFailure::query()->sole();
expect($run->status)->toBe(RunStatus::CompletedWithErrors)
->and($run->rows_created)->toBe(1)
->and($run->rows_failed)->toBe(1)
->and($failure->row_number)->toBe(3)
->and($failure->errors)->toHaveKey('sku')
->and($failure->payload)->toBe([
'SKU' => '',
'Description' => 'Mechanical keyboard',
'Price' => '149.50',
]);
});Row numbers count as the reader counts them, with a header, the first data row is row 2. See row numbers.
A definition-level problem, such as a missing required column or an unmapped attribute, throws instead:
use ByRcsc\LaravelDataSync\Exceptions\SchemaMismatchException;
expect(fn () => DataSync::run('products'))
->toThrow(SchemaMismatchException::class);Every exception the package throws extends DataSyncException, so assert on
that when you care that the run failed rather than on how. See catching package
exceptions.
Two traps come from file identity:
Identical content is the same file. Writing the same CSV twice in one test
produces one import and one skip, not two imports. Vary a value, or pass
force: true:
DataSync::run('products', force: true);A completed checksum survives within the test's transaction. If a test asserts a re-run, assert on the skip rather than expecting a second import:
$second = DataSync::run('products');
expect($second->discovered)->toBe(0)
->and($second->skipped)->toBe(1);minFileAge() compares against the file's modification time, so a file written
by a test is always brand new:
$this->travel(5)->minutes();
DataSync::run('products');Without travelling, a definition with minFileAge(minutes: 2) reports the file
as deferred and imports nothing, which is itself worth one test.
Assert that plain sync:run queues rather than writes:
use ByRcsc\LaravelDataSync\Jobs\ProcessFileJob;
use Illuminate\Support\Facades\Queue;
it('dispatches a file job on the configured queue', function (): void {
Queue::fake();
config()->set('data-sync.overrides.products.queue', ['connection' => 'redis', 'queue' => 'imports']);
Storage::disk('incoming')->put('products/today.csv', "SKU,Description,Price\nDS-1001,Anglepoise desk lamp,89.00\n");
$this->artisan('sync:run products')->assertSuccessful();
Queue::assertPushed(ProcessFileJob::class, fn (ProcessFileJob $job): bool =>
$job->connection === 'redis' && $job->queue === 'imports');
});To exercise the batched path end to end, run the queue synchronously:
config()->set('queue.default', 'sync');Chunk jobs then execute inline and the batch callbacks finalize the run, so the
same assertions as the --now path apply, with the batching code actually
covered.
use ByRcsc\LaravelDataSync\Events\SyncCompleted;
use Illuminate\Support\Facades\Event;
Event::fake([SyncCompleted::class]);
DataSync::run('products');
Event::assertDispatched(SyncCompleted::class, fn (SyncCompleted $event): bool =>
$event->run->rows_created === 2 && ! $event->run->dry_run);Remember that dry runs dispatch events too, assert on dry_run when it
matters.
$report = DataSync::run('products', dryRun: true);
expect($report->runs[0]->created)->toBe(2)
->and(Product::query()->count())->toBe(0)
->and(Storage::disk('archive')->allFiles())->toBeEmpty();A dry run reports what it would do, writes nothing, and leaves the checksum unprocessed, so a follow-up real run imports the same file.
Storage::fake('outgoing');
Storage::disk('incoming')->put('statements/STMT-000431-20260731.pdf', 'binary');
DataSync::run('statements');
expect(Storage::disk('outgoing')->exists('statements/000431/2026/07/STMT-000431-20260731.pdf'))
->toBeTrue();A transfer run reports 1 created, or 1 skipped when the destination already
holds the same checksum or the filename did not match.
php artisan sync:doctorRunning the doctor against your CI environment catches a definition that no
longer resolves, a mapped attribute that a migration removed, and a matchOn()
that lost its unique index, none of which a unit test on one definition would
notice. See health checks.