Introduction

Every Laravel test falls into one of two buckets: feature tests that exercise your app through its public interface (HTTP, console, jobs) and unit tests that exercise a single class in isolation. Knowing which to reach for first is the most important skill in Laravel testing.

Key Concepts

  • Feature test: Boots the full framework, hits a real route, runs middleware and controllers, queries the database.
  • Unit test: Instantiates one class, calls one method, asserts on the return value. No HTTP, no database.
  • Factories: Generate realistic test data for your Eloquent models.
  • RefreshDatabase: A trait that wraps each test in a transaction so state never leaks between tests.

Real World Context

A feature test that posts to /projects and asserts the row lands in the database gives you vastly more confidence than six unit tests mocking a repository. Unit tests shine for value objects, formatters, and pure calculations where booting Laravel is overkill.

Deep Dive

Feature Test Example

This test authenticates as a factory user, posts to a route, and verifies both the redirect and the database row:

php
<?php

use App\Models\User;
use function Pest\Laravel\{actingAs, post};

it('creates a project for the authenticated user', function () {
    $user = User::factory()->create();

    actingAs($user)
        ->post('/projects', [
            'name' => 'Launch Website',
            'status' => 'active',
        ])
        ->assertRedirect('/projects')
        ->assertSessionHas('success');

    expect($user->projects()->count())->toBe(1);
});

One test covers the route, middleware, controller, validation, Eloquent model, and the database. A failure tells you something along that chain broke.

Unit Test Example

Unit tests isolate a single class — no HTTP, no database, no framework boot. Pure logic:

php
<?php

use App\Support\Money;

it('formats amounts with the currency symbol', function () {
    expect(Money::usd(1299)->format())->toBe('$12.99');
});

Factories

Laravel's model factories are the preferred way to build data for tests:

php
// One user, saved to the database
$user = User::factory()->create();

// Ten users with a custom attribute
$admins = User::factory()->count(10)->create(['role' => 'admin']);

// An unsaved model (no database write)
$draft = User::factory()->make();

Common Pitfalls

  1. Unit-testing a controller — Controllers orchestrate framework code. Test them via feature tests where Laravel wires everything up naturally.
  2. Forgetting RefreshDatabase — Without it, data from one test leaks into the next and you get flaky suites. Add the trait (or Pest helper uses(RefreshDatabase::class)) at the top of your feature tests.

Best Practices

  1. Write the feature test first — It tells you whether the feature works from the user's perspective. Add unit tests afterward for gnarly logic.
  2. One behavior per test — A test that asserts five unrelated things is really five tests in a trench coat. Split them.

Summary

  • Feature tests hit real endpoints and are the default in Laravel.
  • Unit tests exercise a single class without the framework.
  • Factories build data; RefreshDatabase keeps the slate clean between tests.
  • Favour feature tests unless you're testing pure logic.

Code Examples

php
<?php

use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use function Pest\Laravel\{actingAs, post};

uses(RefreshDatabase::class);

it('stores a project and redirects', function () {
    $user = User::factory()->create();

    $response = actingAs($user)->post('/projects', [
        'name' => 'Launch Website',
    ]);

    $response->assertRedirect('/projects');
    expect($user->projects()->pluck('name'))->toContain('Launch Website');
});
✓ Completed