In a Laravel warehouse app, tests pass alone but fail when the whole suite runs; how can the database reset traits cause that, and how do you fix it?
answer
- a class with no reset trait
- migrated once, never re-migrated
- second connection outside $connectionsToTransact
- auto-increment keeps climbing after rollback
- assert on models, not hard-coded IDs
basics
~20 sCommitted data or hard-coded assumptions leak between tests: a class without a reset trait, writes on an unwrapped connection, or IDs that keep climbing after rollbacks. Give every database test a trait, list every connection, and assert on created models.
solid answer
~40 sThe reset traits only undo what they wrap. `RefreshDatabase` migrates once per process, so a test class without any reset trait commits rows that every later test in the process sees, which is the docs' own warning. Writes through a connection missing from `$connectionsToTransact`, say a `reporting` connection, are committed too. Rollback also does not rewind auto-increment counters on engines such as MySQL or PostgreSQL, so a test expecting `Product` id 1 passes alone and fails after others. Symptoms are wrong counts, unique-key collisions and "expected 1, got 7" on IDs. Fix the cause: every database test gets a trait, often on `Tests\TestCase` (with `LazilyRefreshDatabase` if many tests never query); every connection written to is listed; tests assert on the models they created, with `assertModelExists($product)` or `$product->id`, never on literal IDs.
code
php · 27 lines<?php
namespace Tests\Feature;
use App\Models\Product;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class PickListTest extends TestCase
{
use RefreshDatabase;
// Writes to both connections must be rolled back.
protected $connectionsToTransact = ['mysql', 'reporting'];
public function test_pick_list_includes_the_product(): void
{
$product = Product::factory()->create(['sku' => 'BOLT-M8']);
// not '/products/1': rolled-back inserts still consume IDs
$this->get("/products/{$product->id}/pick-list")
->assertOk()
->assertSee('BOLT-M8');
$this->assertModelExists($product);
}
}go deeper
Know that every test touching the database needs a reset trait and that tests should use the models they create rather than fixed IDs.
Explain that RefreshDatabase migrates once, so untrait-ed classes leak, and that only listed connections are rolled back.
Diagnose the leak by bisecting test order, spot climbing auto-increment IDs and second connections, and fix causes rather than switching to slow full resets.
Build prevention into the suite: a trait on the base class, review rules on literal IDs, and randomised runs that surface leaks before CI does.
## The symptom A warehouse app's `PickListTest` passes with `php artisan test --filter=PickListTest` and fails in the full run: `assertDatabaseCount('stock_movements', 1)` sees 4, a unique `sku` collides, or a test asserting that the first product has id 1 gets 23. Something from an earlier test is still visible. Laravel's database reset traits are the first suspect, because each has a boundary beyond which it undoes nothing. ## Cause 1: a class without a reset trait `RefreshDatabase` runs `migrate:fresh` only once per PHP process, then relies on transactions. The docs warn that records added by tests that do not use the trait may still exist. So one `ImportTest` class that writes to the database with no trait commits its rows, and every later `RefreshDatabase` test in the same process starts with them. Run alone, the victim never meets them. **Fix:** give every class that touches the database a reset trait. Putting it on `Tests\TestCase` removes the chance to forget; if many tests never query, `LazilyRefreshDatabase` avoids charging them for it. ## Cause 2: a connection outside the transaction `RefreshDatabase` wraps the default connection, or the connections in `$connectionsToTransact`. Code that writes through another connection, such as a `reporting` connection for stock snapshots, commits for real. **Fix:** list every connection the test writes to, for example `protected $connectionsToTransact = ['mysql', 'reporting'];`, or move that test class to `DatabaseTruncation` with `$connectionsToTruncate`. ## Cause 3: IDs that keep climbing A rollback removes rows but, on engines such as MySQL and PostgreSQL, does not give back the auto-increment values those inserts consumed. The first test to create a product gets id 1; after a dozen tests have created and rolled back products, the next one gets id 13. A test that hard-codes `/products/1` or `assertDatabaseHas('stock_movements', ['product_id' => 1])` passes alone and fails in the suite. **Fix:** never assert on literal IDs. Use the models the test created: `"/products/{$product->id}"`, `assertDatabaseHas($product, [...])`, `assertModelExists($movement)`. ## Cause 4: data committed inside the test If the wrapping transaction is no longer open at teardown, because a statement committed it (some engines commit implicitly on schema changes), the rollback has nothing to undo. `RefreshDatabase` notices and clears its migrated flag so the next test migrates again, but the test that committed may already have affected others in between, and code that manages its own commits behaves differently under a wrapping transaction anyway. **Fix:** keep schema changes out of feature tests, and test commit-sensitive code with `DatabaseTruncation`. ## Diagnosing it 1. Run the failing test alone, then with the class that runs just before it; the smallest failing pair points at the leaking class. 2. Look for classes without a reset trait: search the tests directory for classes that create models but use no database trait. 3. Check `assertDatabaseCount` and literal IDs in the failing test; they are the most leak-sensitive assertions. 4. Check which connections the code under test uses, against `$connectionsToTransact`. | Leak | Symptom | Fix | |---|---|---| | class without a trait | extra rows, unique collisions | a trait on every database test | | second connection | rows from another connection's tables | `$connectionsToTransact` or truncation | | climbing auto-increment | "expected id 1" failures | assert on created models | | committed transaction | intermittent re-migration, stray rows | no DDL in tests, truncation for commit-sensitive code | ## Why the pair fails, not each test Each cause needs two ingredients: one test that leaves something behind, and another that assumes a clean slate. Run alone, the second test has no predecessor, so it passes; run alone, the first test asserts nothing about leftovers, so it passes too. That is why fixing only the failing test, for example by relaxing its count, rarely helps: the leak is in the other test, and it will surface again in a different victim. ## Prevention - A reset trait on the base class, so new test classes are covered by default. - Factories inside each test for the data it needs, rather than rows another test left behind. - A review rule against literal IDs in URLs and assertions. - Occasional runs in a different order, which surface leaks before they become flaky failures in CI. Interviewers ask this as a senior scenario because it tests whether a candidate knows what each trait's boundary is, rather than blaming "flaky tests".
- Why doesn't RefreshDatabase clean up rows committed by an earlier test class that had no reset trait?`RefreshDatabase` migrates only once per process and then only rolls back its own transactions. Rows committed before or outside those transactions are never touched. The docs warn about exactly this: records added by tests that do not use the trait may still exist in the database.
- Would switching the whole suite to DatabaseMigrations fix these failures?It would hide some of them, because every test starts from `migrate:fresh`, but it rebuilds the schema per test and slows the suite dramatically. The real fixes are cheaper: a trait on every database test, every written connection listed, and no literal IDs. Keep full resets for the classes that truly need committed data.
saying these in an interview costs you the question
- RefreshDatabase re-migrates before each test, so leaks between tests are impossible.
- A rollback also resets auto-increment counters, so hard-coded IDs are safe.
- RefreshDatabase wraps every configured connection automatically.
- Tests that pass alone and fail together are just flaky and should be retried.
- Only tests that use RefreshDatabase can leave rows behind.