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 testsUnit tests
Lives intests/Feature/tests/Unit/
TestsA slice of the whole application — hitting a route, a middleware chain, a controller, the databaseOne 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 realNot 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?"
SpeedSlower (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.

Up next: Head to Broadcasting & Real-Time to see how Laravel can push live updates to the browser over WebSockets.