Laravel Testing: A Practical Guide
Learn how to choose Laravel test scopes, prepare reliable database state, test HTTP behavior, diagnose failures, and run a suite in parallel.
Laravel testing works best as a repeatable loop: decide what behavior matters, choose the narrowest useful test boundary, prepare predictable state, perform the action, assert what an outside observer can see, and run the suite. Use Unit tests for isolated calculations and Feature tests for behavior that involves the application, such as HTTP requests, middleware, authentication, and persistence. Laravel supports both Pest and PHPUnit, and its Artisan test runner works with either.
This guide uses Laravel 12 documentation for test setup, database testing, and parallel execution. The HTTP testing examples use APIs documented in Laravel 13; check the documentation for your installed Laravel version before copying version-sensitive details. Laravel 12’s upgrade guidance associates that release with PHPUnit 11 and Pest 3, but an existing project should use its installed dependencies rather than upgrade just to follow this guide.
1. Choose the test boundary
Start by writing down the observable behavior: for example, “an authenticated customer can create an order, and the order is stored.” Then select a boundary that exercises enough of the real system to give confidence without involving unrelated components.
| Test type | What it exercises | Good fit | Trade-off |
|---|---|---|---|
| Unit | A small piece of logic in isolation; Laravel says Unit tests do not boot the application. | Calculations, formatting, or rules that do not need framework services. | Fast and focused, but cannot verify integration with the database or framework. |
| Feature | Several objects working together, potentially including a full HTTP request. | Routes, middleware, validation, authentication, persistence, and user-visible behavior. | More setup than a Unit test, but exercises more of the real application. |
Laravel’s guidance says, “Generally, most of your tests should be feature tests.” That is a useful default for application behavior, not a rule that every test must boot the application. Keep pure logic tests isolated when that makes them simpler and more precise. See Laravel 12: Testing: Getting Started.
2. Create and run a test
Generate a Feature test in the default Feature test directory, or add --unit for a Unit test:
php artisan make:test CreateOrderTest
php artisan make:test PriceCalculatorTest --unit
Choose one of Laravel’s supported runners:
php artisan test
vendor/bin/pest
vendor/bin/phpunit
php artisan test runs the configured suite through Artisan. Pest and PHPUnit are both supported; choose the syntax your project already uses and your team can maintain. A generated test is only a starting file: add an action and meaningful assertions before treating it as coverage.
3. Keep the test environment predictable
Tests run in the testing environment. Laravel’s documented defaults set session and cache to the array driver. A project can add .env.testing to override .env values during tests. If you change environment or configuration values while configuration is cached, clear the cache so the test process reads the intended settings.
php artisan config:clear
Keep test credentials and external service settings separate from development and production. Prefer fakes or test-specific implementations for external dependencies, and make the expected test database explicit in the project’s test configuration. Do not point a destructive database reset at production data.
4. Test persistence with factories and database isolation
When a test writes to the database, define its starting state and reset strategy. Laravel’s RefreshDatabase trait resets test state; when the schema is already current, it runs each test inside a transaction instead of migrating the schema for every test. Laravel also documents DatabaseMigrations and DatabaseTruncation, which are significantly slower in its guidance. Use them when their reset semantics fit your setup.
Factories create records that match the application’s model definitions. Use seeders when the behavior specifically depends on seeded application data, rather than adding broad seed data to every test. Assert both the response and the stored outcome when persistence is part of the behavior.
<?php
namespace Tests\Feature;
use App\Models\Order;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class CreateOrderTest extends TestCase
{
use RefreshDatabase;
public function test_customer_can_create_an_order(): void
{
$customer = User::factory()->create();
$response = $this->actingAs($customer)->postJson('/api/orders', [
'sku' => 'book-42',
'quantity' => 2,
]);
$response->assertCreated();
$response->assertJsonPath('data.quantity', 2);
$this->assertDatabaseHas('orders', [
'user_id' => $customer->id,
'sku' => 'book-42',
'quantity' => 2,
]);
}
}
This example assumes the application has an Order model, a user factory, and a JSON route that returns a created response with a data.quantity field. Adapt the route, payload, and response path to the actual contract. The database assertion proves the record persisted; a response assertion alone would not prove that.
Database reset choices are summarized in Laravel 12: Database Testing.
5. Test HTTP behavior and APIs
Laravel HTTP tests make requests to the application and let a test inspect the response without making a real network request to a separately running server. Use the test client to exercise the route boundary where routing, middleware, validation, or serialization is part of the behavior.
public function test_invalid_order_is_rejected(): void
{
$customer = User::factory()->create();
$response = $this->actingAs($customer)->postJson('/api/orders', [
'sku' => '',
'quantity' => 0,
]);
$response->assertUnprocessable();
$response->assertJsonValidationErrors(['sku', 'quantity']);
}
Laravel’s HTTP test APIs cover JSON, uploads, views, sessions, authentication, validation, and response assertions. For an API test, decide what the consumer depends on and assert that contract: status, JSON shape, validation errors, headers, or persisted effects as appropriate. Avoid asserting incidental response details that clients do not rely on. Refer to the versioned Laravel HTTP Tests documentation and verify APIs against your installed framework version.
Authenticated API requests with Sanctum
For a Sanctum-protected endpoint, use Sanctum’s testing helper to authenticate a factory-created user. This keeps the test focused on authorization and endpoint behavior rather than token issuance:
use App\Models\User;
use Laravel\Sanctum\Sanctum;
public function test_authenticated_user_can_read_profile(): void
{
$user = User::factory()->create();
Sanctum::actingAs($user);
$response = $this->getJson('/api/profile');
$response->assertOk();
$response->assertJsonPath('data.id', $user->id);
}
Adapt the route and JSON path to the application. Add a separate unauthenticated test if the endpoint must reject guests. See Laravel 12: Sanctum testing.
6. Assert outcomes that matter
A useful test fails when the behavior regresses and points toward what broke. Match assertion scope to the requirement:
- HTTP contract: status, JSON fields, validation errors, redirect, or view data.
- Persistence: database row exists, is absent, or has the expected values.
- Authorization: permitted users succeed and disallowed users receive the expected rejection.
- Side effects: assert that a notification, job, or event was dispatched using the relevant Laravel testing facilities.
- Pure logic: input and output cases, including boundary values and invalid inputs.
Prefer assertions about externally meaningful outcomes over implementation details such as a private method call. For stateful behavior, cover both the successful path and at least the important rejection or boundary case.
7. Run tests in parallel when the suite is ready
First make sequential runs reliable. Parallel workers use more database and process resources, and tests that share files, ports, or external state need coordination. Laravel 12 documents installing ParaTest as a development dependency and enabling parallel execution like this:
composer require brianium/paratest --dev
php artisan test --parallel
php artisan test --parallel --processes=4
Laravel creates and migrates a separate test database per process when a primary database is configured. The database name includes a process token. Databases persist between runs unless you pass --recreate-databases:
php artisan test --parallel --recreate-databases
For shared non-database resources, Laravel provides ParallelTesting setup and teardown hooks, including process tokens that can partition resource names. Use them to give each process distinct files, cache keys, or other shared resources where needed. Tune process count to available CPU, memory, and database capacity; more workers can increase contention rather than reduce elapsed time. See Laravel 12 parallel testing for setup and lifecycle details.
8. See what the browser rendered
HTTP tests verify application responses at the framework boundary. When a bug depends on the rendered page, a screenshot can provide a visual artifact for review alongside those tests. A captured image is useful for inspection, but it does not replace assertions or automatically prove that a page is correct.
Or skip the browser setup
If you need a rendered page artifact without setting up browser capture, ScreenshotNeo accepts one GET request with a URL and returns an image or PDF. See the ScreenshotNeo API documentation.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
- Cookie banners are accepted and removed before capture; known newsletter popups and chat widgets are removed too, and each step can be turned off.
- Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing. Responses identify the page verdict and billing status in headers.
- An MCP server provides
take_screenshot,get_page_info, andcapture_pdftools for AI agents and MCP clients. - The Free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 shots. Every feature is on every plan.
Sign up for 1,000 free screenshots a month, with no card.
9. Troubleshooting common failures
| Symptom | Likely cause | Fix |
|---|---|---|
| Test cannot find a table or column | The test database schema is stale or points to the wrong database. | Check the testing connection and migrations, then use the project’s chosen reset strategy. |
| Rows leak between tests | Tests do not reset state, or code writes outside the transaction/reset mechanism. | Use RefreshDatabase where appropriate and check for external connections or side effects. |
| Tests unexpectedly use cached settings | Configuration cache was built before test settings changed. | Clear configuration cache and verify the testing environment values. |
| API test gets an authentication error | The request lacks the expected authenticated user or uses the wrong auth mechanism. | Authenticate through the application or the package’s documented test helper, such as Sanctum::actingAs. |
| JSON assertion fails despite a successful status | The actual response shape differs from the guessed field path. | Inspect the response and assert the documented API contract’s exact JSON path. |
| Parallel tests collide on files or shared resources | Workers are using the same resource name or location. | Partition the resource by process token with parallel testing hooks, or run that work sequentially. |
| Parallel run is slower or unstable | Worker count exceeds available CPU, memory, or database capacity, or setup overhead dominates. | Reduce process count, isolate shared resources, and compare against a stable sequential run. |
10. A practical test checklist
- State the behavior and expected observable result before writing the test.
- Use Unit tests for isolated logic and Feature tests for behavior that crosses application components.
- Use factories for focused records and seeders only when the scenario needs seeded data.
- Reset persistence deliberately and keep testing configuration isolated.
- Assert both the response contract and database effects when both matter.
- Run sequentially first; parallelize only with database and shared-resource isolation in place.
- Check versioned documentation when copying framework or package-specific APIs.
Frequently asked questions
Do I need Pest to test a Laravel application?
No. Laravel supports Pest and PHPUnit. Use the runner and style already configured for the project.
Should every endpoint have a Feature test?
Test endpoints whose behavior matters to users or clients, especially authorization, validation, and persistence contracts. Choose focused assertions that protect those contracts.
Does a screenshot test replace an HTTP test?
No. A screenshot is a visual artifact. An HTTP test can assert status, response data, authentication, and persisted state.
When should I use database seeders in tests?
Use a seeder when the behavior depends on the application’s seeded data. For a small scenario-specific setup, a factory is usually more direct.


