skip to content

Automated App Checks

Laravel's test layer over PHPUnit and Pest: php artisan test, HTTP assertions, database reset traits and facade fakes. Interviewers check you can test an app without real I/O.

on this pageshow

explore

questions

27

In Laravel, what does the RefreshDatabase trait do to the test database, and why is it the usual default for feature tests?

level: juniorimportance: must knowfreq 72%

answer

  1. migrate once, then transactions
  2. migrate:fresh on first use per process
  3. rollback when the app is torn down
  4. :memory: SQLite keeps one PDO
  5. tests without the trait can leave rows

basics

~20 s

RefreshDatabase 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
<?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

for a junior

Recall that RefreshDatabase migrates once and wraps each test in a transaction that is rolled back, so every test starts clean.

for a middle

Explain the static migrated flag, the rollback in teardown, the :memory: PDO reuse, and $connectionsToTransact for extra connections.

for a senior

Name what the trait cannot undo: committed writes from untrait-ed tests, other connections, other processes, and implicit commits, and pick another trait there.

for a principal

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.
open as a page

In a Laravel feature test, how do you request a sneaker shop's checkout routes and assert the status, redirect and view data?

level: juniorimportance: must knowfreq 72%

basics

~10 s

Call $this->get() or $this->post() inside a Tests\TestCase test; each returns a TestResponse. Chain assertOk() or assertStatus(), assertRedirect() or assertRedirectToRoute(), and assertViewIs() or assertViewHas() for the Blade data.

open as a page

In a Laravel 13 app, how do tests in tests/Feature differ from tests in tests/Unit, and which base class does each extend?

level: juniorimportance: must knowfreq 68%

basics

~10 s

Feature tests extend Tests\TestCase, which boots the whole Laravel application before every test, so HTTP calls, the database, facades and the container work. The skeleton's unit tests extend PHPUnit\Framework\TestCase directly and boot nothing.

open as a page

In a Laravel feature test, how do you assert that issuing a refund queues a job and sends a mailable without running or sending either?

level: middleimportance: must knowfreq 64%

basics

~10 s

Call Queue::fake() (or Bus::fake()) and Mail::fake() before the request, then assert with Queue::assertPushed(ProcessRefund::class) and Mail::assertSent(RefundIssued::class). The fakes record instead of pushing or delivering, so neither the job nor the mail runs.

open as a page

In a Laravel feature test, what does actingAs($user) do, and how does it differ from logging in through the login route?

level: middleimportance: must knowfreq 60%

basics

~20 s

actingAs($user) puts the user straight onto a guard and makes it the default for the rest of the test, skipping credentials, session login and the Login event. Use it for what sits behind login; test login itself through the login route.

open as a page

A Laravel subscription-box app's test suite takes twenty minutes; how would you use php artisan test --profile and --parallel to shorten it?

level: seniorimportance: must knowfreq 40%

basics

~20 s

Profile first with php artisan test --profile to find the slowest tests and fix their causes, then install brianium/paratest and run php artisan test --parallel, which gives each process its own database, cache prefix and compiled-view folder.

open as a page

In a Laravel test, how do assertDatabaseHas, assertDatabaseCount and assertModelExists check that a warehouse stock movement was saved?

level: juniorimportance: should knowfreq 55%

basics

~20 s

assertDatabaseHas('stock_movements', [...]) passes when at least one row matches every given column; assertDatabaseCount checks the table's row count; assertModelExists($movement) checks the model's primary key is still in its table. Each has a Missing or Empty counterpart.

open as a page

In Laravel 13, how do you set up Laravel Dusk and write a first browser test that signs a new user up?

level: juniorimportance: should knowfreq 30%

basics

~20 s

Require laravel/dusk as a dev dependency and run php artisan dusk:install, which creates tests/Browser and DuskTestCase and downloads ChromeDriver. Point APP_URL at the running app, chain visit(), type() and press() inside $this->browse(), and run php artisan dusk.

open as a page

In a Laravel test, how do you move the clock forward to check that a refund request window closes after 14 days?

level: juniorimportance: should knowfreq 44%

basics

~10 s

Use the test case's time helpers: $this->travel(15)->days() or $this->travelTo($date) set Carbon's test time, so now() and Eloquent timestamps see the new date. travelBack() or a closure restores real time, and teardown resets it anyway.

open as a page

In Laravel, what does php artisan test add over vendor/bin/phpunit, and how does make:test decide which file it writes?

level: juniorimportance: should knowfreq 52%

basics

~20 s

php artisan test runs the same Pest or PHPUnit suite with readable output, forwards runner options and adds --parallel and --profile. make:test writes a tests/Feature test by default, a tests/Unit one with --unit, and Pest files when Pest is detected.

open as a page

How do Laravel's RefreshDatabase, DatabaseTransactions, DatabaseMigrations, DatabaseTruncation and LazilyRefreshDatabase differ, and when would you choose each?

level: middleimportance: should knowfreq 45%

basics

~20 s

RefreshDatabase 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.

open as a page

In Laravel tests, how do #[Seed], #[Seeder], the $seed property and $this->seed() seed data, and when does each one run?

level: middleimportance: should knowfreq 34%

basics

~20 s

#[Seed] or $seed = true makes the reset trait's migrate:fresh run DatabaseSeeder; #[Seeder(X::class)] or $seeder runs one class instead. With RefreshDatabase that happens once per process and every test rolls back to it. $this->seed() runs seeders inside one test.

open as a page

Why should a Laravel Dusk test never use RefreshDatabase or an in-memory SQLite database, and which trait do you use instead?

level: middleimportance: should knowfreq 32%

basics

~20 s

RefreshDatabase keeps each test's writes in an uncommitted transaction, but Dusk's browser hits a separate server process with its own connection, which cannot see them; :memory: SQLite is per-process too. Use DatabaseTruncation or DatabaseMigrations on a shared database.

open as a page

In a Laravel test, what does Event::fake() stop from running, and how do you fake only some events?

level: middleimportance: should knowfreq 42%

basics

~10 s

Event::fake() swaps the dispatcher for a recorder, so no listener runs, including Eloquent model-event listeners and observers. Pass a list, Event::fake([RefundIssued::class]), to fake only those; everything else dispatches normally.

open as a page

In a Laravel test, how do Cache::shouldReceive(), a facade spy and $this->mock() differ when you replace a collaborator?

level: middleimportance: should knowfreq 48%

basics

~10 s

Cache::shouldReceive() swaps the facade's instance for a Mockery mock with expectations set up front; a spy records every call for shouldHaveReceived() afterwards; $this->mock(Service::class) binds a Mockery mock in the container for injected classes.

open as a page

In Laravel HTTP tests, how do assertJson, assertExactJson, assertJsonPath and fluent AssertableJson differ when checking a checkout API response?

level: middleimportance: should knowfreq 45%

basics

~20 s

assertJson checks a loose subset of the body; assertExactJson needs the whole body to match; assertJsonPath checks one path strictly with assertSame; a closure given to assertJson gets an AssertableJson that fails on unchecked keys unless etc() is called.

open as a page

In Laravel HTTP tests, why does an invalid checkout sent with post() redirect while postJson() returns 422, and which assertions fit each?

level: middleimportance: should knowfreq 50%

basics

~20 s

postJson() sends Accept: application/json, so a validation failure renders as a 422 JSON response; post() sends a form request, so Laravel redirects back with errors flashed to the session. Use assertSessionHasErrors or assertJsonValidationErrors respectively, or assertInvalid for either.

open as a page

In Laravel, how do phpunit.xml <env> entries, a .env.testing file and the regular .env decide which database a test run touches?

level: middleimportance: should knowfreq 46%

basics

~20 s

phpunit.xml sets APP_ENV=testing and its other <env> values before Laravel boots. Laravel then loads .env.testing instead of .env if it exists, never overwriting variables already set, so phpunit.xml beats .env.testing, unless a cached config bypasses all of it.

open as a page

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?

level: seniorimportance: should knowfreq 32%

basics

~20 s

Committed 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.

open as a page

A Laravel Dusk sign-up test passes locally but fails intermittently in CI after press('Sign up'). How do you make it reliable?

level: seniorimportance: should knowfreq 28%

basics

~20 s

Dusk's press() and assertions never wait, so on a slower CI machine they run before the page updates. Replace pause() with condition waits such as waitForLocation() or waitForText(), and target elements through dusk attributes like @signup-button.

open as a page

In a Laravel test suite, how do Http::fake() and Http::preventStrayRequests() keep tests from calling a real payment provider's refund API?

level: seniorimportance: should knowfreq 38%

basics

~10 s

Http::fake() answers requests made through Laravel's HTTP client with stubs and records them for Http::assertSent(). Http::preventStrayRequests() makes any request without a matching stub throw a StrayRequestException instead of reaching the network.

open as a page

Why can calling withoutMiddleware() make a Laravel checkout test pass while the real endpoint fails, and what should you do instead?

level: seniorimportance: should knowfreq 26%

basics

~20 s

withoutMiddleware() with no arguments skips every global and route middleware, so authentication, authorization, throttling, sessions and route model binding all stop running. The test then checks a different endpoint than production; disable one class with withoutMiddleware(Foo::class), or satisfy the middleware.

open as a page

When a Laravel Dusk test fails only in CI, which artefacts does Dusk leave behind and how do you use them to find the cause?

level: middleimportance: nice to knowfreq 20%

basics

~20 s

On failure Dusk saves a screenshot per browser to tests/Browser/screenshots; after every browse() it writes non-empty console logs to tests/Browser/console, plus page source after source assertions. Keep them as CI artefacts and read them before changing code.

open as a page

In Laravel Dusk, how does $browser->loginAs($user) authenticate the browser, and what must you watch for when using it?

level: middleimportance: nice to knowfreq 18%

basics

~20 s

loginAs() makes the browser visit Dusk's /_dusk/login/{userId} route, whose controller logs the user in and sets the session cookie. Those routes exist in every non-production environment, and the session persists into later tests in the same file.

open as a page

In Laravel 13, how do Sleep::fake() and Str::freezeUuids() make a refund retry's back-off and generated reference testable?

level: middleimportance: nice to knowfreq 22%

basics

~10 s

Sleep::fake() makes Laravel's Sleep class record durations instead of pausing, so back-off can be asserted with Sleep::assertSequence(). Str::freezeUuids() makes Str::uuid() return one known value; Laravel 13 resets these Str factories after each test.

open as a page

In a Laravel Dusk sign-up test, how do you fill a birth-date field driven by a JavaScript date picker with a read-only input?

level: seniorimportance: nice to knowfreq 15%

basics

~20 s

Operate the widget: click the input, waitFor() the calendar, click the day via dusk selectors, then assert. type() cannot fill a read-only input, and value() writes the DOM without input or change events, so bound JavaScript state misses it.

open as a page

In Laravel parallel testing, what do the ParallelTesting hooks and ParallelTesting::token() give you, and when do you need them?

level: seniorimportance: nice to knowfreq 18%

basics

~20 s

ParallelTesting hooks run closures on process or test-case setup and teardown, and when a per-process test database is created, only in parallel runs. token() returns the worker's identifier for separating files, indexes or Redis databases, and false outside a parallel run.

open as a page