Introduction

Testing is the fastest way to know your code works — not just today, but after every refactor, dependency bump, and feature addition. Laravel ships with both PHPUnit and Pest preinstalled and a phpunit.xml already configured, so there's no setup friction between cloning a repo and writing your first assertion.

Key Concepts

  • Unit test: Tests a single class or function in isolation with no framework boot.
  • Feature test: Tests a whole feature via HTTP requests, with the full framework booted.
  • Browser test: Tests a real browser flow via Laravel Dusk (Chromium + WebDriver).
  • PHPUnit vs Pest: Two testing frameworks Laravel supports out of the box — PHPUnit is the class-based classic, Pest is the top-level function-style modern option.

Real World Context

Every production Laravel app needs tests: refactors without tests are bets, not changes. New engineers pick up a codebase faster when they can read the tests as executable documentation. And CI gates driven by tests catch regressions before they hit the main branch.

Deep Dive

Why Test?

BenefitDescription
ConfidenceKnow your code works before deploying
Refactoring SafetyChange code without breaking features
DocumentationTests show how code should behave
Faster DevelopmentCatch bugs early, not in production
Better DesignTesting encourages better architecture

Types of Tests

┌─────────────────────────────────────────────────────────┐
│                      E2E Tests                          │
│              (Browser, full user flows)                 │
│                     Slowest, fewest                     │
├─────────────────────────────────────────────────────────┤
│                    Feature Tests                        │
│            (HTTP requests, database)                    │
│                   Medium speed & count                  │
├─────────────────────────────────────────────────────────┤
│                     Unit Tests                          │
│              (Single class/function)                    │
│                    Fastest, most                        │
└─────────────────────────────────────────────────────────┘
  • Unit Tests: Test isolated pieces (a single class or method)
  • Feature Tests: Test features (HTTP requests, multiple classes)
  • Browser Tests: Test through a real browser (Laravel Dusk)

Test Structure in Laravel

tests/
├── Feature/             # Feature tests
│   ├── ExampleTest.php
│   └── PostTest.php
├── Unit/                # Unit tests
│   ├── ExampleTest.php
│   └── UserTest.php
├── TestCase.php         # Base test class
└── CreatesApplication.php

PHPUnit vs Pest

Laravel supports both testing frameworks:

PHPUnit (Traditional)

php
<?php

namespace Tests\Feature;

use Tests\TestCase;

class ExampleTest extends TestCase
{
    public function test_the_application_returns_a_successful_response(): void
    {
        $response = $this->get('/');

        $response->assertStatus(200);
    }
}

Pest (Modern, Fluent)

php
<?php

test('the application returns a successful response', function () {
    $response = $this->get('/');

    $response->assertStatus(200);
});

Running Tests

bash
# Run all tests
php artisan test

# With PHPUnit directly
./vendor/bin/phpunit

# Run specific test file
php artisan test tests/Feature/PostTest.php

# Run specific test method
php artisan test --filter test_user_can_view_posts

# Run tests in parallel
php artisan test --parallel

# Stop on first failure
php artisan test --stop-on-failure

# With coverage report
php artisan test --coverage

Creating Tests

bash
# Create a feature test
php artisan make:test PostTest

# Create a unit test
php artisan make:test UserTest --unit

# Create a Pest test
php artisan make:test PostTest --pest

Test Environment

Tests use the testing environment:

php
// phpunit.xml sets:
<env name="APP_ENV" value="testing"/>
<env name="DB_DATABASE" value=":memory:"/>  // SQLite in memory

You can also create .env.testing for test-specific configuration.

Basic Assertions

php
// PHPUnit assertions
$this->assertTrue($value);
$this->assertFalse($value);
$this->assertEquals($expected, $actual);
$this->assertNull($value);
$this->assertNotNull($value);
$this->assertCount(3, $array);
$this->assertContains('item', $array);
$this->assertInstanceOf(User::class, $user);

// Pest assertions
expect($value)->toBeTrue();
expect($value)->toBeFalse();
expect($actual)->toBe($expected);
expect($actual)->toEqual($expected);
expect($value)->toBeNull();
expect($array)->toHaveCount(3);
expect($array)->toContain('item');
expect($user)->toBeInstanceOf(User::class);

Your First Test

php
<?php

namespace Tests\Unit;

use App\Models\User;
use PHPUnit\Framework\TestCase;

class UserTest extends TestCase
{
    public function test_user_full_name(): void
    {
        $user = new User([
            'first_name' => 'John',
            'last_name' => 'Doe',
        ]);

        $this->assertEquals('John Doe', $user->fullName);
    }

    public function test_user_is_admin_by_default_false(): void
    {
        $user = new User();

        $this->assertFalse($user->is_admin);
    }
}

With Pest:

php
<?php

use App\Models\User;

test('user full name', function () {
    $user = new User([
        'first_name' => 'John',
        'last_name' => 'Doe',
    ]);

    expect($user->fullName)->toBe('John Doe');
});

test('user is not admin by default', function () {
    $user = new User();

    expect($user->is_admin)->toBeFalse();
});

Common Pitfalls

  1. Using Tests\TestCase for pure unit tests — that base class boots the whole Laravel container, slowing a microsecond test to a millisecond. For pure PHP logic, extend PHPUnit\Framework\TestCase instead.
  2. Running tests against your development database — a test that wipes the posts table will wipe your dev posts too. Always configure a separate testing database in phpunit.xml (SQLite :memory: is perfect).

Best Practices

  1. Run php artisan test instead of vendor/bin/phpunit — same runner, but better output, parallel support via --parallel, and consistent with the rest of Laravel's CLI.
  2. Pick one framework per file, but mix across the suite — PHPUnit and Pest both work; consistency within a file matters more than the global choice.

Summary

  • Laravel ships with PHPUnit and Pest preinstalled and a phpunit.xml configured.
  • Unit tests go in tests/Unit, feature tests in tests/Feature, browser tests in tests/Browser.
  • php artisan test is the canonical runner; --filter, --parallel, --coverage are common flags.
  • Pure unit tests should extend PHPUnit\Framework\TestCase for speed.
✓ Completed