Testing
An application you can't verify automatically is an application you're afraid to change. Laravel takes
testing seriously enough to ship PHPUnit pre-configured out of the box — a fresh
Laravel project already has a phpunit.xml config file, a tests/ directory,
and example tests ready to run the moment you install it. This page covers the two kinds of tests
Laravel distinguishes between, how to test routes and the database together, and a couple of testing
conveniences (factories and facade faking) that make writing tests much less tedious than it sounds.
Feature Tests vs. Unit Tests
Laravel's tests/ directory is split into two folders, and the distinction is about scope,
not importance — both matter, but they test very different things.
| Feature tests | Unit tests | |
|---|---|---|
| Lives in | tests/Feature/ | tests/Unit/ |
| Tests | A slice of the whole application — hitting a route, a middleware chain, a controller, the database | One class or method in isolation, with nothing else involved |
| Bootstraps the framework? | Yes — the full app is booted, so routing, middleware, and the database are all real | Not necessarily — a pure Unit test can run without touching Laravel at all |
| Good for | "Does visiting /articles return a 200 and show the right content?" | "Does this one helper method format a string correctly?" |
| Speed | Slower (more machinery involved) | Very fast |
In practice, most of the valuable tests in a typical Laravel app are Feature tests, simply because most of what can go wrong in a web application involves several pieces working together (a route, a middleware, a database query) rather than a single isolated method.
Generating a Test
Artisan scaffolds a new test class for you, wired up with the right base class and namespace already:
php artisan make:test ArticleTest
By default this creates a Feature test at tests/Feature/ArticleTest.php. Add
--unit to generate a Unit test in tests/Unit/ instead:
php artisan make:test ArticleTest --unit
A Full Feature Test Example
A Feature test typically makes a simulated HTTP request against a route and asserts something about the response — exactly like a real browser would experience it, but scripted and repeatable:
<?php
namespace Tests\Feature;
use Tests\TestCase;
class ArticleTest extends TestCase
{
public function test_articles_index_page_loads()
{
$response = $this->get('/articles');
$response->assertStatus(200);
$response->assertSee('Articles');
}
}
$this->get() simulates a GET request through the entire application — routing,
middleware, the controller, the view — and returns a TestResponse object with a whole
family of readable assertion methods. assertStatus(200) checks the HTTP status code,
and assertSee() checks that a given string appears somewhere in the rendered HTML body,
which is a quick way to confirm the right content actually made it to the page.
Testing With a Real Database
Many Feature tests need actual data in the database to test against — but you never want one test's
leftover data bleeding into the next test. The RefreshDatabase trait solves this: add it
to a test class, and Laravel migrates a clean database before the test class runs and wraps every
individual test in a transaction that's rolled back afterward, so each test starts from a known-empty
state.
<?php
namespace Tests\Feature;
use App\Models\Article;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ArticleTest extends TestCase
{
use RefreshDatabase;
public function test_articles_index_shows_seeded_articles()
{
Article::factory()->create(['title' => 'My Article']);
$response = $this->get('/articles');
$response->assertStatus(200);
$response->assertSee('My Article');
}
}
Article::factory()->create() is the same model factory mechanism covered in
Migrations & Seeders — inside a test it's the fastest way
to get one realistic row into the database without hand-writing a full Article::create([...])
call with every column filled in. Any attributes you pass to create(), like
title here, override the factory's random defaults for just that call.
Asserting Against the Database Directly
Sometimes the thing you actually care about isn't what a page displays, but whether a row ended up
correctly in the database — for example, after submitting a form. assertDatabaseHas()
queries the database directly and fails the test if no matching row exists:
<?php
public function test_storing_an_article_saves_it_to_the_database()
{
$this->post('/articles', [
'title' => 'My Article',
'body' => 'Some content.',
]);
$this->assertDatabaseHas('articles', [
'title' => 'My Article',
]);
}
This is a much stronger assertion than just checking the HTTP response — it proves the data actually
landed in the articles table, columns and all, rather than assuming a "success" message
in the response means the database write actually happened correctly.
Running the Test Suite
Laravel provides its own thin Artisan wrapper around PHPUnit that produces nicer output, but the raw PHPUnit binary works too:
# Laravel's own test runner (recommended, nicer output)
php artisan test
# Or call PHPUnit directly
./vendor/bin/phpunit
# Run just one test file
php artisan test --filter=ArticleTest
Faking Facades: Mail::fake() and Friends
A test that sends a real email every time it runs is slow, unreliable (what if the mail server is
down?), and annoying (you don't want a real inbox full of test emails). Laravel solves this with
fakes — swap-in test doubles for common facades that record what "would have"
happened without actually doing it. Mail::fake(), called at the top of a test, replaces
the real mailer with a fake one for the rest of that test:
<?php
use App\Mail\ArticlePublished;
use Illuminate\Support\Facades\Mail;
public function test_publishing_an_article_sends_a_notification_email()
{
Mail::fake();
$this->post('/articles/1/publish');
Mail::assertSent(ArticlePublished::class);
}
No real email leaves the server, the test runs fast, and Mail::assertSent() still proves
your controller logic actually attempted to send the right mailable — the same pattern extends to
Queue::fake(), Event::fake(), and Notification::fake() for
jobs, events, and notifications respectively.