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
- Unit-testing a controller — Controllers orchestrate framework code. Test them via feature tests where Laravel wires everything up naturally.
- Forgetting
RefreshDatabase— Without it, data from one test leaks into the next and you get flaky suites. Add the trait (or Pest helperuses(RefreshDatabase::class)) at the top of your feature tests.
Best Practices
- Write the feature test first — It tells you whether the feature works from the user's perspective. Add unit tests afterward for gnarly logic.
- 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;
RefreshDatabasekeeps the slate clean between tests. - Favour feature tests unless you're testing pure logic.
Code Examples
<?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');
});