Essential Laravel Testing: A Complete Overview

Because bugs are just features we forgot to test.


In modern Laravel development, testing is no longer a luxury. It’s a necessity. This article walks you through the most essential types of tests you can (and should) implement in your Laravel 12 application, powered by PHP 8.4.


🧪 1. Unit Tests

Goal: Test a class or a method in isolation.

Location: tests/Unit

Example:

use App\Services\VatCalculator;

test('it calculates the VAT correctly', function (): void {
    $calculator = new VatCalculator();

    $result = $calculator->calculate(100.0, 20.0);

    expect($result)->toBe(120.0);
});

Best practice: Use clear method names like it_calculates_vat_correctly, and avoid dependencies (e.g., database).


🧩 2. Feature Tests

Goal: Test the integration of multiple components: routes, controllers, views, database.

Location: tests/Feature

Example:

test('user can create a blog post', function (): void {
    $user = User::factory()->create();

    $this->actingAs($user)
        ->post('/posts', [
            'title' => 'Hello World',
            'content' => 'My first blog post',
        ])
        ->assertRedirect('/posts');
});

Tip: Use traits like RefreshDatabase to isolate each test run.


🌐 3. HTTP / API Tests

Goal: Simulate RESTful API calls and assert response structure and status.

Example:

test('API returns paginated users', function (): void {
    User::factory()->count(30)->create();

    $response = $this->getJson('/api/users');

    $response->assertStatus(200)
             ->assertJsonStructure([
                 'data' => [['id', 'name', 'email']],
                 'links', 'meta',
             ]);
});

Useful for: Auth headers, error formats, validation, pagination.


🔗 4. Integration Tests

Goal: Verify collaboration between multiple components (e.g., controller + service + DB).

Often confused with Feature Tests, but here you want to make sure the wiring between classes works as expected.


🎮 5. End-to-End (E2E) / Browser Tests

Tool: Laravel Dusk

Goal: Simulate full browser interactions from the user’s point of view.

Example:

$this->browse(function (Browser $browser): void {
    $browser->visit('/login')
            ->type('email', 'john@example.com')
            ->type('password', 'secret')
            ->press('Login')
            ->assertPathIs('/dashboard');
});

Downside: Slower. Needs real or headless browsers.


🏛️ 6. Architecture / Contract Tests

Goal: Enforce architectural rules across your codebase.

Tool: spatie/pest-plugin-architecture

Example:

uses(Spatie\Architecture\Expectations\ArchTest::class);

it('Controllers should only use Services or Requests')
    ->expect('App\Http\Controllers')
    ->toUse('App\Services')
    ->ignoring('Illuminate\Http\Request');

Why: Maintain clear boundaries and avoid spaghetti code.


📸 7. Snapshot Tests

Goal: Ensure that output (JSON, HTML, etc.) does not change unexpectedly.

Tool: spatie/phpunit-snapshot-assertions

Example:

test('returns expected JSON structure', function (): void {
    $response = $this->getJson('/api/posts');

    $response->assertMatchesJsonSnapshot();
});

🔁 8. Test-Driven Development (TDD)

Cycle: Red -> Green -> Refactor

  1. Write a failing test

  2. Make it pass

  3. Refactor

Example:

test('calculates discount for VIP', function (): void {
    $service = new DiscountService();

    $result = $service->calculate(100.0, 'vip');

    expect($result)->toBe(90.0); // 10% off
});

TDD helps you write only the code you need—no more, no less.


⚗️ 9. Bonus: Mutation, Fuzzing & Performance Tests

  • Mutation Testing: Use Infection to verify if your tests detect common bugs.

  • Fuzzing: Throw random inputs at your functions to test stability.

  • Performance: Benchmark long-running processes or APIs.


✅ Conclusion

You don’t have to write all the tests—but you should aim for the ones that deliver maximum confidence with minimum maintenance. At a minimum:

  • Use Unit tests for logic

  • Feature tests for user flows

  • API tests for endpoints

  • Architecture tests for boundaries

Want confidence in your code? Test it like you mean it.


Happy testing, artisan!

0
Subscribe to my newsletter

Read articles from Jean-Marc Strauven directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Jean-Marc Strauven
Jean-Marc Strauven

Jean-Marc (aka Grazulex) is a developer with over 30 years of experience, driven by a passion for learning and exploring new technologies. While PHP is his daily companion, he also enjoys diving into Python, Perl, and even Rust when the mood strikes. Jean-Marc thrives on curiosity, code, and the occasional semicolon. Always eager to evolve, he blends decades of experience with a constant hunger for innovation.