How do Laravel's RefreshDatabase, DatabaseTransactions, DatabaseMigrations, DatabaseTruncation and LazilyRefreshDatabase differ, and when would you choose each?
answer
- transaction versus real reset
- DatabaseTransactions never migrates
- DatabaseMigrations: fresh plus rollback per test
- DatabaseTruncation: truncates before later tests
- Lazily: refresh on first query
basics
~20 sRefreshDatabase migrates once and rolls each test back; DatabaseTransactions only rolls back and never migrates; DatabaseMigrations migrates fresh and rolls back per test; DatabaseTruncation migrates once then truncates tables before later tests; LazilyRefreshDatabase is RefreshDatabase deferred until the first query.
solid answer
~40 sAll five reset state, but by two mechanisms. The **transaction** family (`RefreshDatabase`, `LazilyRefreshDatabase`, `DatabaseTransactions`) wraps each test in a rolled-back transaction: `RefreshDatabase` also runs `migrate:fresh` once per process, `LazilyRefreshDatabase` does the same only when the test first queries the database, and `DatabaseTransactions` assumes the schema already exists. The **real reset** family commits data for real: `DatabaseMigrations` runs `migrate:fresh` before each test and `migrate:rollback` after it, and `DatabaseTruncation` migrates on the first test and truncates every table except the migrations table before each later one, optionally reseeding. Choose transactions by default for speed; choose truncation when another process or connection must see the data, such as browser tests; `DatabaseMigrations` is rarely worth its cost.
code
php · 19 lines<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\DatabaseTruncation;
use Tests\TestCase;
class ReportExportTest extends TestCase
{
use DatabaseTruncation;
// Only these tables are cleared before each later test.
protected $tablesToTruncate = ['stock_movements', 'products'];
public function test_nightly_export_reads_committed_rows(): void
{
// Rows are committed, so a separate connection or process can read them.
}
}go deeper
Know that RefreshDatabase is the default and that DatabaseMigrations and DatabaseTruncation reset fully but are slower.
Explain rollback versus real reset, that DatabaseTransactions never migrates, and when truncation happens relative to each test.
Choose by visibility and cost: truncation only for a named second process or connection, lazy refresh for mixed suites, and never DatabaseMigrations by habit.
Set the suite's default trait and the exception process, weighing CI time against the fidelity of committed-data tests.
## Two mechanisms Every Laravel database reset trait answers one question: how does the next test get a clean database? There are two answers. - **Roll back.** Open a transaction before the test and roll it back after. Fast, but only works when everything the test writes goes through connections that the trait wrapped, and nothing outside that transaction needs to see the data. - **Really reset.** Let the test commit, and clean the database with DDL or truncation. Slower, but data is visible to anything that reads the database. ## The five traits | Trait | Before each test | After each test | Migrates | |---|---|---|---| | `RefreshDatabase` | begin transaction | roll back | `migrate:fresh` once per process | | `LazilyRefreshDatabase` | nothing until the first query or transaction, then as `RefreshDatabase` | roll back | once per process, lazily | | `DatabaseTransactions` | begin transaction | roll back | never; schema must exist | | `DatabaseMigrations` | `migrate:fresh` | `migrate:rollback` | every test | | `DatabaseTruncation` | first test: `migrate:fresh`; later tests: truncate tables, reseed if configured | nothing | once per process | A few details from the source that interviews reach for: 1. `DatabaseTransactions` has no migration step at all. It suits a database whose schema is prepared elsewhere, and it ignores seeding attributes. 2. `LazilyRefreshDatabase` registers callbacks on the connection that fire before the first query or transaction. A test that never touches the database pays nothing, which makes it a good choice when the trait sits on a shared base class. 3. `DatabaseTruncation` truncates **before** a test, not after, skips the migrations table, and accepts `$tablesToTruncate` or `$exceptTables` to narrow the list. It disables foreign-key checks while truncating. 4. The transaction traits install a testing transaction manager, so callbacks registered to run after commit are not held back by the invisible wrapping transaction. ## When each one fits For a warehouse app with a few thousand feature tests: - **Default: `RefreshDatabase`.** Nearly every HTTP and model test runs inside one process and one connection, so rollback is correct and cheap. - **`LazilyRefreshDatabase`** when the trait lives on `Tests\TestCase` and many tests never query, such as validation-only or pure-logic feature tests. - **`DatabaseTruncation`** when another process or connection must read the committed rows: browser tests driving a separately served app, or code that deliberately opens a second connection outside `$connectionsToTransact`. The docs present it, with `DatabaseMigrations`, as the choice when you want to reset the database totally, and as significantly slower than `RefreshDatabase`. - **`DatabaseTransactions`** for a database whose schema is managed outside the suite, or when migrating from scratch is impossible. - **`DatabaseMigrations`** almost never: it rebuilds the whole schema for every test, which on a large migration history can dominate a suite's run time. ## Costs to weigh - **Speed.** A transaction costs almost nothing; truncating dozens of tables costs a query per table; `migrate:fresh` costs every migration. - **Visibility.** Uncommitted rows are invisible to other connections and processes; truncation and migrations make them visible. - **Fidelity.** Transaction wrapping changes one thing about the code under test: its own transactions become nested inside the wrapper. Code that depends on real commit behaviour needs a real-reset trait to be tested faithfully. - **Leftovers.** Truncation cleans at the start of the next test, so data from the last test in a run stays in the database afterwards, which is harmless for a dedicated test database and a problem for a shared one. ## Mixing traits in one suite Different classes may use different traits, and they share one piece of state: the process-wide migrated flag. That has a consequence worth knowing. `DatabaseTruncation` cleans **before** each of its own tests, so the rows committed by the last truncation test are still there when the next class runs. If that next class uses `RefreshDatabase`, which never truncates, its tests start with those rows. Keep truncation classes' data self-contained, or give them a narrow `$tablesToTruncate` and assertions that tolerate unrelated rows, and be suspicious of counts in transaction-based tests that run after them. ## A rule of thumb Start with `RefreshDatabase`. Move a test class to `DatabaseTruncation` only when you can name the second process or connection that needs to see its data, and to `LazilyRefreshDatabase` when profiling shows migrations charged to tests that never query. Interviewers ask this to see whether a candidate picks by mechanism rather than by habit.
- Why can a test using DatabaseTransactions fail with 'no such table' while RefreshDatabase works?`DatabaseTransactions` only opens and rolls back a transaction; it never migrates. If the test database has no schema yet, the first query fails. `RefreshDatabase` runs `migrate:fresh` once per process before its transactions, so it works on an empty database.
- When is LazilyRefreshDatabase better than RefreshDatabase on a shared base test class?When many tests in the suite never touch the database. `LazilyRefreshDatabase` defers the migration and transaction until the first query or transaction on the connection, so those tests pay nothing, while tests that do query get exactly the `RefreshDatabase` behaviour.
saying these in an interview costs you the question
- DatabaseTransactions migrates the schema before wrapping the test.
- DatabaseTruncation truncates tables after each test finishes.
- DatabaseMigrations is the fastest choice because it starts from scratch.
- Rolled-back rows are visible to a browser test's separate server process.
- All five traits differ only in speed, never in what other processes see.