Testing · Open source
Refresher
A drop-in replacement for Laravel's RefreshDatabase that restores a cached snapshot of your test database instead of running migrate:fresh.
What it solves
RefreshDatabase runs migrate:fresh, plus your seeders if you seed, once in every test process: every single test you run while you work, every --parallel worker and every CI job.
On a real app that is hundreds of CREATE TABLE statements before the first assertion.
The first test process migrates and seeds as usual and saves the database as a snapshot. Every process after that restores the snapshot, until a migration, seeder or factory changes. Your tests, factories and assertions do not change.
- Drop-in traits for RefreshDatabase and LazilyRefreshDatabase, for PHPUnit and Pest
- Seeders run once and the seeded database is what gets saved
- Snapshot key covers migrations (including package paths), seeders and factories, the driver and the migrate:fresh options
- The key uses file names and contents, so a snapshot cached in CI matches on every runner
- Always restores a real snapshot or migrates; never assumes the database is still migrated
- SQLite file databases, MySQL and MariaDB
Install
Requires PHP 8.3+ and Laravel 12 and 13.
composer require amjadiqbal/refresher --dev
Usage
// tests/Pest.php
use AmjadIqbal\Refresher\Testing\RefreshesWithRefresher;
pest()->extend(Tests\TestCase::class)
->use(RefreshesWithRefresher::class) // was: RefreshDatabase::class
->in('Feature');
Setup
Swap the trait. For MySQL and MariaDB the mysqldump and mysql client programs must be on the machine that runs the tests. Each test still runs inside a transaction that is rolled back, and $seed, $seeder, $dropViews, $dropTypes, $connectionsToTransact and php artisan test --parallel keep working.
use AmjadIqbal\Refresher\Testing\RefreshesWithRefresher;
class OrderTest extends TestCase
{
use RefreshesWithRefresher; // was: use RefreshDatabase;
}
LazilyRefreshDatabase
The database is only restored once a test actually runs a query, exactly like LazilyRefreshDatabase.
pest()->extend(Tests\TestCase::class)->use(LazilyRefreshesWithRefresher::class)->in('Feature');
When it re-migrates
A snapshot is stored under a hash of every migration file (database/migrations plus paths registered with loadMigrationsFrom()), your seeders and factories when the tests seed, and the database driver and migrate:fresh options. Change any of those and the next run migrates fresh and saves a new snapshot. php artisan refresher:clear forces a fresh migration.
Caching snapshots in CI
Snapshots are written to storage/framework/refresher (REFRESHER_SNAPSHOT_PATH). Cache that directory to reuse a snapshot across jobs. A stale cache is harmless: if the hash does not match, Refresher migrates and saves a new snapshot.
- uses: actions/cache@v4
with:
path: storage/framework/refresher
key: refresher-${{ hashFiles('database/migrations/**', 'database/seeders/**', 'database/factories/**') }}
Drivers and limits
- SQLite: a snapshot is a copy of the database file. In-memory SQLite has no file, so it migrates in every process as before.
- MySQL and MariaDB: snapshots are made with mysqldump and restored with the mysql client, after every table and view is dropped. The password goes through MYSQL_PWD, never on the command line.
- PostgreSQL is not supported; other drivers fail with an UnsupportedDriverException.
- Only the database is in the snapshot. Files a seeder writes are not recreated on restore.
- Restoring is not free: with a small schema and no seeders, the saving is small.
Hire me
Need this built into your app, or something custom?
I wrote Refresher. I can wire it into your Laravel app, extend it for your case, or build the feature around it. Tell me what you need and you'll get a plan and a quote, free.
Support
Found a bug or want a feature? Open an issue on GitHub. For help adding it to your own project, use the quote form above.
More Laravel packages
All packages
Laravel Driver.js
Build Driver.js product tours in PHP and render them with one Blade component.
Laravel Sonner
Sonner toast notifications for Laravel, sent from PHP and shown without React.
Laravel Ninja Keys
A Cmd+K / Ctrl+K command palette for Laravel, with the actions defined in PHP.
Laravel SweetAlert2
SweetAlert2 popups, toasts and confirm dialogs from Laravel controllers, Livewire and Alpine.
Laravel Grid.js
Turn any Eloquent query into a searchable, sortable, paginated Grid.js data table with one fluent PHP call.
Laralink
Work on a Composer package inside the Laravel app that uses it, then switch back to the Packagist release.
Kiln
Artisan commands and an optional signed HTTP route to clear, warm and inspect OPcache when you deploy.
Laravel Media Viewer
Show private PDFs, video and audio from any Laravel disk without making the files public.
Laravel Swiper
Swiper sliders, carousels and image galleries for Laravel as Blade components.
Laravel MySQL Advisor
Read-only health checks for the MySQL or MariaDB database behind your Laravel app, with the SQL and the Laravel migration to fix a problem wherever a fix applies.
Laravel Webhook Console
A dashboard and replay tool for the webhooks spatie/laravel-webhook-client stores in your Laravel app.