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?
| Benefit | Description |
|---|---|
| Confidence | Know your code works before deploying |
| Refactoring Safety | Change code without breaking features |
| Documentation | Tests show how code should behave |
| Faster Development | Catch bugs early, not in production |
| Better Design | Testing 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
- Using
Tests\TestCasefor pure unit tests — that base class boots the whole Laravel container, slowing a microsecond test to a millisecond. For pure PHP logic, extendPHPUnit\Framework\TestCaseinstead. - 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
- Run
php artisan testinstead ofvendor/bin/phpunit— same runner, but better output, parallel support via--parallel, and consistent with the rest of Laravel's CLI. - 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.xmlconfigured. - Unit tests go in
tests/Unit, feature tests intests/Feature, browser tests intests/Browser. php artisan testis the canonical runner;--filter,--parallel,--coverageare common flags.- Pure unit tests should extend
PHPUnit\Framework\TestCasefor speed.