Skip to content

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.

Laravel 12 and 13 PHP 8.3+ v1.1.0 Free · MIT
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.

Need something built?

Free quote in 60 seconds