Testing Laravel applications
Status: Complete. Last reviewed 2026-08-27.
Precise mental model
Section titled “Precise mental model”A Laravel test is a claim about an observable boundary while deliberately deciding which framework and infrastructure components are real. The important distinction is not “unit tests are good, feature tests are slow”; it is whether the test includes the mechanisms that could violate the claim. A pure PHP unit test can precisely exercise a value object. A Laravel feature test boots the application and can exercise routing, middleware, validation, authorization, container wiring, Eloquent, and serialization together.
Laravel’s HTTP test methods send a simulated request through the application rather than opening a real network connection. They prove application behavior but not web-server, proxy, TLS, or deployment configuration. CSRF middleware is disabled during tests, and Laravel recommends one request per test because multiple requests do not recreate normal request isolation reliably. Browser or deployed smoke tests own those outer boundaries.
The generic ideas behind test pyramids, doubles, property tests, and mutation testing live in Testing and software quality. This page concentrates on Laravel-specific confidence gaps: the application boot process, database reset traits, factories, facades and fakes, transactions, parallel workers, and framework lifecycle state.
Choose the boundary from the failure risk
Section titled “Choose the boundary from the failure risk”Start with the invariant and enumerate the path that could break it. If the claim is “an authenticated tenant cannot update another tenant’s project,” an HTTP feature test should normally traverse the real route, middleware, binding, Form Request, policy, database query, and resource. Mocking the controller’s service and asserting a method call would skip the dangerous path. Assert the response, persisted state, and absence of unauthorized side effects.
Conversely, a pricing rule with many input combinations may be clearer and faster as a framework-free unit test. Test the application service directly when HTTP parsing is already covered and the risk lies in orchestration. Add a small number of real-driver or deployed tests for boundaries Laravel’s process cannot simulate. Layers are complementary: choose the narrowest test that still includes the mechanism under suspicion, then add a wider contract test where a fake creates a meaningful blind spot.
Laravel stores conventional framework-free tests in tests/Unit and application tests in tests/Feature; generated test cases use Pest or PHPUnit on top of the same Laravel testing facilities. In Laravel 13, the UnitTest PHP attribute can also mark an individual test method to run without booting the application. Directory names and syntax do not create isolation—the actual base class, traits, container use, and I/O do.
Database state, factories, and engine fidelity
Section titled “Database state, factories, and engine fidelity”RefreshDatabase is the normal choice for database-backed feature tests. It migrates when the test schema is not current, then runs eligible tests inside database transactions. That is fast and keeps rows created during the test from surviving it. Data created outside the trait’s transaction, or through another connection, can still leak. A suite should not depend on test order or on rows left by another case.
DatabaseMigrations rolls migrations back and reruns them between tests. DatabaseTruncation migrates once, then truncates tables between tests. Both are slower than transaction-based reset but are useful when the behavior must cross a real commit boundary or when transaction coverage is incomplete. Pick the reset mechanism from what must be observed, not by habit.
That matters for after-commit work. A test wrapped in RefreshDatabase may never commit its outer transaction, so an event, listener, notification, or queued job configured to run after commit may not become observable. Do not “fix” the product by removing after-commit safety. Put commit-sensitive behavior in a focused test using a reset strategy and connection arrangement that permits a real commit, then assert the outcome.
Factories should name relevant states and relationships so the scenario reads like its invariant. Keep the important values explicit; uncontrolled faker randomness turns failures into archaeology. Factories can also run model events, so creating fixtures after Event::fake() may suppress behavior on which the factory relies. Seeders suit stable reference data, not a hidden global scenario shared by unrelated tests.
SQLite is useful for a fast inner loop, but use the production database engine when the claim depends on its semantics: locks and concurrency, transaction isolation, JSON operators, collations, generated columns, type coercion, constraints, or raw SQL. “All feature tests pass on SQLite” is not evidence that a MySQL or PostgreSQL query and migration behave identically.
Fakes define confidence boundaries
Section titled “Fakes define confidence boundaries”Laravel fakes replace an outgoing subsystem with a recorder or in-memory implementation. They are excellent for proving that application code requested the correct effect without sending mail, making HTTP calls, or starting workers. They do not prove the replaced system accepts, transports, serializes, or executes that request.
Queue::fake() proves dispatch details and can fake selected jobs or allow selected jobs through. It does not run a queued job. A separate test that invokes the job’s real handle() path can prove its business effect and idempotency, but still does not prove payload serialization, queue-driver configuration, worker boot, middleware, retry timing, timeout behavior, or after-commit dispatch. Cover high-risk queue paths with a real-driver integration or worker smoke test as well. See the confidence-boundary example.
The same reasoning applies across framework fakes:
Event::fake()prevents listeners from running. Fake after factories if their model events are required, or fake only the events under test.Mail::fake()andNotification::fake()prove send intent and recipients, not provider credentials, deliverability, or final provider rendering.Storage::fake()proves filesystem interactions against a test disk, not object-store permissions, consistency, metadata, or signed URLs.Http::fake()makes integrations deterministic. Pair it withHttp::preventStrayRequests()so an unregistered call cannot reach the network unnoticed, and retain contract tests against a controlled provider or sandbox.
Time helpers such as travel(), freezeTime(), and freezeSecond() make expiry logic deterministic and restore time after their callbacks. They do not simulate a sleeping worker, database-server clock, or another process. Assert the clock-dependent contract at the process boundary that owns it.
Transactions, queues, and believable side effects
Section titled “Transactions, queues, and believable side effects”Database writes and external effects have different commit rules. Laravel can defer queued jobs, listeners, mailables, notifications, and broadcasts until a database transaction commits. This prevents a fast worker from reading uncommitted or later-rolled-back state. A rollback discards the deferred dispatch.
A useful testing split for a queued export is:
- an HTTP feature test fakes the queue and proves the authorized request commits the intended job identity;
- a job test executes the handler against realistic database rows and fakes only the external provider, proving idempotent domain effects;
- a commit-aware integration test proves rollback suppresses dispatch and commit releases it;
- a small real queue/worker smoke test proves serialization and deployment wiring.
No single test must carry every cost, but together they cover the failure timeline. This subject overlaps queues only at the testing boundary; queue design, retry policy, delivery semantics, and worker operations belong to the next dedicated bundle.
Suite-only failures and parallel isolation
Section titled “Suite-only failures and parallel isolation”Laravel boots a fresh application for each normal test method, but the PHP process and external services may survive. Order-dependent failures often come from static properties, facade fakes, container singletons, mutated configuration, global exception handlers, frozen time, open transactions, or files and cache keys shared between tests. First reproduce the failure with the same seed/order and process settings, then run the smallest failing pair in both orders. Remove implicit setup and restore every modified global.
Parallel testing creates a separate test database for each process and appends a process token. That protects the default database path; it does not automatically namespace Redis, cache keys, locks, queue names, object-storage prefixes, local files, ports, or third-party sandbox accounts. Use Laravel’s parallel lifecycle hooks and token to isolate each external namespace, or serialize tests that cannot be safely partitioned. Run the suite repeatedly and in parallel in CI; a flaky green result is not a passed invariant.
Ordinary feature tests also do not reproduce a long-running worker serving many jobs or Octane requests. Process-scoped singletons and static state may look harmless because the application is rebuilt per test. Add a focused lifecycle test that handles two tenants or jobs in the same process when reset behavior matters.
Pest, PHPUnit, static analysis, and CI
Section titled “Pest, PHPUnit, static analysis, and CI”Pest and PHPUnit are both supported test runners in current Laravel applications. Pest’s functions and datasets can reduce ceremony; PHPUnit classes and attributes may fit established suites and tooling. They can coexist while a codebase migrates. Consistent naming, fixtures, and assertion intent matter more than choosing one syntax everywhere.
Larastan extends PHPStan with knowledge obtained by booting the Laravel container. It catches type paths that runtime examples may miss, including uncertain relation values, container resolution assumptions, and impossible branches, but it does not execute policies, SQL, or side effects. A useful CI sequence runs formatting checks, static analysis, fast unit tests, database-backed feature tests, and selected real-boundary tests. Coverage reports where code executed; they do not show whether assertions distinguish correct from incorrect behavior.
Test configuration comes from phpunit.xml and optionally .env.testing. Cached configuration can cause tests to ignore changed environment values, so clear the configuration cache when the observed environment disagrees with the files. Treat CI service versions, extensions, database engine, and queue configuration as versioned parts of the test environment.
Failure timeline: the fake queue stays green
Section titled “Failure timeline: the fake queue stays green”- An HTTP test calls
Queue::fake()and asserts thatExportProjectwas pushed with a project ID. - Production serializes the job after the request transaction, but its payload contains a stale model graph and tenant context is process-local.
- A worker boots without the request’s tenant state and resolves the identifier from the wrong scope.
- The job retries after an external upload succeeded but before the local completion row committed, duplicating the file.
- The fake-based test stays green because it exercised none of serialization, worker boot, scoping, or redelivery.
- The repair stores explicit scalar identity, restores tenant context before scoped lookup, makes the effect idempotent, preserves after-commit dispatch, tests the real handler twice, and adds a real-worker smoke path.
Current and legacy context
Section titled “Current and legacy context”Current (Laravel 13): new applications include Pest and PHPUnit support; feature tests boot the framework while unit tests can avoid it; method-level UnitTest and seeding attributes are available; parallel testing manages per-process test databases; and Laravel provides focused fakes plus stray HTTP-request prevention.
Common (Laravel 11–12): RefreshDatabase, factories and states, HTTP assertions, facade fakes, time travel, parallel testing, and Pest/PHPUnit coexistence remain familiar. Exact generated skeletons and testing dependencies vary by application creation date.
Legacy: older suites may extend framework test cases for every unit, share seeded databases, depend on SQLite despite production-specific SQL, or mock deep chains of facades and Eloquent builders. Characterize important behavior first, then introduce explicit factory states, reliable reset traits, outcome assertions, static analysis, and a few real-boundary tests incrementally.
Interview practice
Section titled “Interview practice”- LARAVEL-TESTING-01 — Choose a Laravel test boundary
- LARAVEL-TESTING-02 — Design an HTTP confidence test
- LARAVEL-TESTING-03 — Choose database reset and engine fidelity
- LARAVEL-TESTING-04 — Expose a queue-fake blind spot
- LARAVEL-TESTING-05 — Use Laravel fakes without fictional confidence
- LARAVEL-TESTING-06 — Diagnose a suite-only or parallel failure