In Laravel, what does the RefreshDatabase trait do to the test database, and why is it the usual default for feature tests?
answer
- migrate once, then transactions
- migrate:fresh on first use per process
- rollback when the app is torn down
- :memory: SQLite keeps one PDO
- tests without the trait can leave rows
basics
~20 sRefreshDatabase runs migrate:fresh the first time a test in the process needs it, then wraps every test in a database transaction that is rolled back afterwards. Each test starts from the same clean schema without paying for a migration per test.
solid answer
~50 s`RefreshDatabase` combines a one-off migration with per-test transactions. On the first test that uses it in a PHP process it runs `migrate:fresh` (seeding too, if configured) and remembers that in a static flag. For every test it then begins a transaction on the default connection, or on each connection listed in `$connectionsToTransact`, and rolls it back when the test's application is torn down, so whatever the test wrote disappears. With SQLite `:memory:`, it also keeps the migrated PDO connection and hands it to each new test, because a fresh in-memory connection would be an empty database. It is the default because it is fast and needs no cleanup code. Its limits: it cannot undo data committed outside that transaction, and the docs warn that rows written by tests without the trait may still be there.
code
php · 25 lines<?php
namespace Tests\Feature;
use App\Models\Product;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ReceiveStockTest extends TestCase
{
use RefreshDatabase;
public function test_receiving_stock_records_a_movement(): void
{
$product = Product::factory()->create(['sku' => 'BOLT-M8']);
$this->post("/products/{$product->id}/receive", ['quantity' => 50]);
$this->assertDatabaseHas('stock_movements', [
'product_id' => $product->id,
'quantity' => 50,
]);
// rolled back when the test ends
}
}go deeper
Recall that RefreshDatabase migrates once and wraps each test in a transaction that is rolled back, so every test starts clean.
Explain the static migrated flag, the rollback in teardown, the :memory: PDO reuse, and $connectionsToTransact for extra connections.
Name what the trait cannot undo: committed writes from untrait-ed tests, other connections, other processes, and implicit commits, and pick another trait there.
Weigh SQLite speed against production fidelity and decide which suites must run on the production engine before merging.
## The problem it solves Feature tests in a Laravel warehouse app create products, stock movements and purchase orders. If those rows survived, the next test would start with unknown data: counts would be off, unique constraints would collide, and results would depend on the order tests ran in. A **database reset trait** puts the database back into a known state for every test. `Illuminate\Foundation\Testing\RefreshDatabase` is the one the docs introduce first and the one most suites use. ## What it does, step by step When a test class `use`s the trait, the base test case calls its `refreshDatabase()` method during setup: 1. **Migrate once per process.** If a static `RefreshDatabaseState::$migrated` flag is false, it runs `php artisan migrate:fresh`, which drops every table and runs all migrations, then sets the flag. Later tests in the same PHP process skip this step. 2. **Open a transaction.** It begins a transaction on each connection it manages, by default the one named in `database.default`. 3. **Run the test.** Everything the test and the application write goes into that open transaction. 4. **Roll back.** When the test's application is destroyed, the trait rolls the transaction back and disconnects. The docs summarise this as: the trait does not migrate your database if your schema is up to date; it only runs the test in a transaction. ## The in-memory SQLite special case The Laravel 13 skeleton's `phpunit.xml` points tests at SQLite with `DB_DATABASE=:memory:`. An in-memory database lives only as long as its PDO connection, and every test builds a new application with new connections. To avoid migrating before every test, `RefreshDatabase` stores the migrated PDO object in `RefreshDatabaseState::$inMemoryConnections` and puts it back into the connection at the start of each test. That is why `:memory:` plus `RefreshDatabase` is fast: one migration, then one transaction per test. The trade-off is fidelity. SQLite is not the database the warehouse app runs on in production, so behaviour that depends on the production engine's SQL dialect, types or locking can pass in tests and fail in production. Many teams run the suite against the production engine in CI for that reason. ## Why it is the default | Property | Effect | |---|---| | One migration per process | no per-test schema cost | | Rollback per test | no cleanup code in tests | | Works with factories and HTTP tests | the request and the assertions share the transaction | | Handles `:memory:` | fast local runs out of the box | The alternatives, `DatabaseMigrations` and `DatabaseTruncation`, reset the database fully and are, in the docs' words, **significantly slower**. ## Where it stops protecting you - **Tests without the trait.** A test class that writes to the database without any reset trait commits its rows, and because `RefreshDatabase` does not re-migrate within the process, later tests see them. The docs call this out explicitly. - **Other connections.** Only the connections in `$connectionsToTransact` are wrapped. Writes through a second connection, such as a `reporting` connection, are committed for real. - **Other processes.** A queue worker, a browser-driven test or anything else reading the database through its own connection cannot see uncommitted rows. That is where truncation-based traits come in. - **Committed work inside the test.** If the wrapping transaction is no longer active when the test ends, for example because a schema statement committed it on an engine that does that implicitly, the trait resets its flag so the next test migrates again. ## Reading a failure under the trait Because everything happens inside one transaction, a failing assertion leaves no trace in the database afterwards: opening a database client after the run shows empty tables. To inspect state, dump it from inside the test, for example with `dump(Product::all()->toArray())` just before the failing assertion, or temporarily switch the class to `DatabaseTruncation`, whose rows stay until the next test starts. Remember to switch back, because the transaction trait is what keeps the suite fast. ## In practice Put `use RefreshDatabase;` on every feature test class that touches the database, or on `Tests\TestCase` if nearly all of them do. Create data with factories inside each test rather than relying on another test's rows. When a test needs something the trait cannot provide, such as visibility from another process, switch that class to a different reset trait instead of adding manual cleanup. A junior candidate should know that `RefreshDatabase` gives each test a clean database through a rollback; interviewers then probe what it cannot undo.
- Why is RefreshDatabase with SQLite :memory: fast even though each test boots a new application?An in-memory database disappears with its connection, so a naive setup would migrate before every test. `RefreshDatabase` migrates once, stores the PDO connection in `RefreshDatabaseState::$inMemoryConnections`, and restores it into each new test's connection. Every test then only opens and rolls back a transaction.
- Your code writes through a second database connection; how do you keep RefreshDatabase in control?Add a `$connectionsToTransact` property listing every connection the test touches, for example `['mysql', 'reporting']`. The trait then opens and rolls back a transaction on each. Without it, only the default connection is wrapped, and the other connection's writes are committed and leak into later tests.
RefreshDatabase is like writing on a whiteboard over a printed template: the template is printed once, and after each lesson you wipe only what was written, which is far quicker than reprinting the board every time.
saying these in an interview costs you the question
- RefreshDatabase runs migrate:fresh before every single test.
- RefreshDatabase deletes rows after each test with DELETE statements.
- RefreshDatabase also rolls back writes made through every other connection.
- A queue worker process can see the rows a RefreshDatabase test inserted.
- Tests without the trait cannot affect tests that use it.