aslain.dev
0%
01 Hizmetler 02 Hakkımda 03 Projeler 04 Stack 05 Blog 06 İletişim
← Tüm makaleler Web Development

Laravel Testing: A Practical PHPUnit and Pest Guide

Writing Laravel tests is the single habit that turns a project from a pile of code that merely “seems to work” into a codebase you can change with confidence. Tests tell you instantly which behaviour a refactor broke; they act as living documentation for the next developer; and most importantly, they remove the “did I break something?” anxiety from every deploy. In this guide we'll cover Laravel's two test engines — the default PHPUnit and the increasingly popular Pest with its cleaner syntax — through practical examples.

How testing is set up in Laravel

A fresh Laravel project already ships with testing. The phpunit.xml file at the root defines the test environment, while the tests/ folder splits into two directories: tests/Unit and tests/Feature. The distinction is conceptual:

  • Unit tests exercise a single class or method in isolation, without booting the full framework. They are fast and focus on pure logic (calculations, formatting, small services).
  • Feature tests verify an HTTP request, a route, the database and several classes working together, end to end. These usually deliver the most real-world value.

The most practical way to run them is Laravel's own wrapper:

php artisan test
# or directly:
./vendor/bin/phpunit
# to filter a single test:
php artisan test --filter=ProjectCanBeCreated

In phpunit.xml the test environment is typically configured with APP_ENV=testing and an in-memory SQLite database (DB_CONNECTION=sqlite, DB_DATABASE=:memory:). That way tests run against a clean schema every time, without touching your real database.

Writing your first feature test

Artisan is enough to scaffold a test class:

php artisan make:test ProjectTest          # tests/Feature
php artisan make:test PriceTest --unit      # tests/Unit

A simple feature test starts by confirming that a page loads correctly. With PHPUnit syntax:

<?php

namespace Tests\Feature;

use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;

class ProjectTest extends TestCase
{
    use RefreshDatabase;

    public function test_homepage_loads_successfully(): void
    {
        $response = $this->get('/');

        $response->assertStatus(200);
        $response->assertSee('Aslain');
    }
}

Here $this->get('/') simulates a real HTTP request; assertStatus and assertSee verify the response. The test method name must start with test_ (or be marked with the #[Test] attribute) for PHPUnit to recognise it.

Database tests and RefreshDatabase

For tests that touch the database, the RefreshDatabase trait is essential: it resets the schema inside a transaction before each test and rolls it back afterwards, so tests never bleed into each other. To produce data you use factories:

public function test_project_is_persisted(): void
{
    $project = Project::factory()->create([
        'title' => ['en' => 'Test Project'],
    ]);

    $this->assertDatabaseHas('projects', [
        'id' => $project->id,
    ]);
}

assertDatabaseHas queries a row directly. Its counterpart assertDatabaseMissing is handy when testing deletions. Factories live under database/factories and generate realistic random data via the fake() helper.

Authentication, JSON and form submissions

In real applications most routes require an authenticated user. Laravel handles this in a single line with actingAs:

public function test_authorised_user_can_add_project(): void
{
    $user = User::factory()->create();

    $response = $this->actingAs($user)->post('/admin/projects', [
        'title_en' => 'New Project',
    ]);

    $response->assertRedirect('/admin/projects');
    $this->assertDatabaseHas('projects', ['title->en' => 'New Project']);
}

For JSON-based APIs there are methods like getJson and postJson, plus structure-aware assertions such as assertJson / assertJsonStructure:

$this->getJson('/api/projects')
     ->assertOk()
     ->assertJsonCount(3, 'data')
     ->assertJsonStructure(['data' => [['id', 'title']]]);

Writing the same tests more concisely with Pest

Pest is a testing framework built on top of PHPUnit; the engine is identical, the syntax is more fluent. Instead of classes and methods you write function-based tests. The same feature test looks like this in Pest:

<?php

use App\Models\User;

it('lets an authorised user add a project', function () {
    $user = User::factory()->create();

    $this->actingAs($user)
        ->post('/admin/projects', ['title_en' => 'New Project'])
        ->assertRedirect('/admin/projects');

    expect(Project::count())->toBe(1);
});

The expect() expression provides a readable assertion chain (toBe, toBeTrue, toContain…). Traits like RefreshDatabase are applied to a whole directory at once via uses() in the tests/Pest.php file. To set Pest up, add the pestphp/pest package with Composer and run php artisan pest:install; your existing PHPUnit tests keep running side by side untouched.

Faking dependencies: mocks and fakes

Good tests never really connect to external services (email, payment, notifications). Laravel ships ready-made fakes for this. For example, you can assert a notification was sent without dispatching a real email:

use Illuminate\Support\Facades\Mail;

Mail::fake();

// ... action ...

Mail::assertSent(WelcomeMail::class);

The same idea applies to Queue::fake(), Event::fake(), Storage::fake() and Http::fake(). These keep tests fast and deterministic — they no longer depend on a network connection or the state of a third-party service.

Frequently Asked Questions

Should I pick PHPUnit or Pest?

Both use the same engine, so it isn't a contest of technical superiority. If you want a class-based, familiar and enterprise-friendly structure, choose PHPUnit; if you prefer less boilerplate and readable syntax, Pest makes sense. For new projects Pest is increasingly becoming the default, but your team's habits should decide.

Should I test everything?

No. Aiming for 100% coverage is usually wasteful. Test the business-critical flows first (payment, registration, authorisation, orders) and the spots that have broken before. Writing tests for plain getters/setters or the framework's own code is wasted effort.

Why are my tests slow?

The most common reasons are hitting a real database instead of in-memory SQLite, and not faking external services. With :memory: SQLite, RefreshDatabase and proper fakes, a test suite can run in seconds.

Want to bake testing into your project? If you need help setting up a safe test suite on your existing Laravel app, all the way to CI/CD integration, get in touch — let's harden your codebase together.

Bu kategorideki tüm yazılar →

Devamı için