Laravel'de PHPUnit ve Pest ile Test Yazma Rehberi

Laravel projelerinde test yazmak çoğu geliştiricinin ertelediği ama üretimde en çok işe yarayan alışkanlıklardan biridir. Bu rehber, orta seviye bir L...

Laravel'de PHPUnit ve Pest ile Test Yazma Rehberi

Laravel projelerinde test yazmak çoğu geliştiricinin ertelediği ama üretimde en çok işe yarayan alışkanlıklardan biridir. Bu rehber, orta seviye bir Laravel geliştiricisinin PHPUnit ve Pest PHP ile test altyapısını kurmasına, anlamlı testler yazmasına ve yaygın tuzaklardan kaçınmasına odaklanır. Sürüme bağlı davranış farkları olduğunda bunu açıkça belirtiyorum; kesin sürüm numarası vermediğim yerlerde güncel Laravel sürümlerinde geçerli olan genel davranışı anlatıyorum.

Önkoşullar ve kapsam

  • Çalışan bir Laravel projesi ve Composer bağımlılıkları kurulu olmalı.
  • PHP ve Laravel sürümlerinin uyumlu olduğunu varsayıyorum. Laravel 11 ve sonrasında uygulama iskeleti sadeleşti; test yapılandırması phpunit.xml üzerinden yürür. Daha eski sürümlerde bazı dosya konumları ve varsayılanlar farklı olabilir.
  • Laravel, phpunit/phpunit paketini geliştirme bağımlılığı olarak getirir. Pest kullanacaksanız ayrıca pestphp/pest ve Laravel eklentisini kurmanız gerekir.
  • Bu rehber birim (unit) ve özellik (feature) testlerine odaklanır; tarayıcı tabanlı testler (örneğin Laravel Dusk) kapsam dışıdır.

Test türlerini ayırt etmek

Laravel iki temel kategoriyi ayrı tutar:

  • Unit testleri: Tek bir sınıfı veya fonksiyonu izole biçimde doğrular. Uygulamayı ayağa kaldırmaz, veritabanına dokunmaz. Hızlıdır.
  • Feature testleri: HTTP isteği, route, middleware, veritabanı ve oturum gibi bileşenleri birlikte çalıştırır. Gerçeğe daha yakındır ama daha yavaştır.

Pratik kural: İş kuralı içeren saf mantığı unit testiyle, kullanıcıya görünen davranışı feature testiyle doğrula. Her şeyi feature testine yığmak test paketini yavaşlatır; her şeyi unit testine indirmek ise entegrasyon hatalarını kaçırır.

İlk testi yazmak (PHPUnit)

Laravel'de test sınıfları tests/ altında yaşar. tests/Unit ve tests/Feature dizinleri varsayılan olarak ayrılmıştır. Yeni bir test oluşturmak için Artisan kullanabilirsiniz:

php artisan make:test UserRegistrationTest
php artisan make:test UserRegistrationTest --unit

İlk komut feature, ikincisi unit testi üretir. Üretilen sınıf Tests\TestCase temel sınıfından türer; bu sınıf Laravel uygulamasını test için hazırlar.

Basit bir feature testi şöyle görünür:

<?php

namespace Tests\Feature;

use Tests\TestCase;

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

        $response->assertStatus(200);
    }
}

$this->get() bir HTTP isteği simüle eder ve dönen TestResponse nesnesi üzerinden iddialar (assertion) yaparsınız. assertStatus(200) yanıt kodunu doğrular. Beklenen davranış: Route tanımlıysa ve erişim engellenmiyorsa 200 döner; route yoksa test başarısız olur ve eksik tanımı size gösterir.

Veritabanı kullanan testler

Veritabanına dokunan testlerde her testin temiz bir durumdan başlaması gerekir. Laravel bunun için trait'ler sunar:

  • RefreshDatabase: Testleri transaction içinde çalıştırır veya migration'ları tazeler. En yaygın tercihtir.
  • DatabaseTransactions: Migration'ları bir kez çalıştırır, her testi transaction ile sarar. Şema değişmiyorsa daha hızlıdır.
<?php

namespace Tests\Feature;

use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class UserProfileTest extends TestCase
{
    use RefreshDatabase;

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

        $response = $this->actingAs($user)->put('/profile', [
            'name' => 'Yeni İsim',
        ]);

        $response->assertRedirect('/profile');
        $this->assertDatabaseHas('users', [
            'id' => $user->id,
            'name' => 'Yeni İsim',
        ]);
    }
}

Burada üç şey oluyor: User::factory() ile sahte bir kullanıcı üretiliyor, actingAs() ile o kullanıcı oturum açmış gibi davranılıyor, assertDatabaseHas() ise güncellemenin gerçekten kaydedildiğini doğruluyor. Beklenen davranış: Doğrulama kuralları geçerse kayıt güncellenir ve yönlendirme döner; doğrulama başarısızsa 302 yerine 422 döner ve test kırılır.

Önemli uyarı: RefreshDatabase gerçek veritabanı bağlantınızı kullanır. Testleri asla üretim veritabanına karşı çalıştırmayın. phpunit.xml içinde ayrı bir test veritabanı (örneğin SQLite in-memory veya ayrı bir MySQL şeması) tanımlayın. Yanlış yapılandırılmış bir test ortamı, veri kaybına yol açabilecek en yaygın hatalardan biridir.

Pest PHP'ye geçiş

Pest, PHPUnit üzerine kurulu bir test framework'üdür; aynı motoru kullanır ama daha kısa ve okunaklı bir sözdizimi sunar. Kurulum genellikle Composer üzerinden yapılır ve Pest'in Laravel eklentisiyle birlikte gelir:

composer require pestphp/pest pestphp/pest-plugin-laravel --dev
php artisan pest:install

Kurulumdan sonra PHPUnit testleriniz çalışmaya devam eder; Pest mevcut testleri kırmaz. Aynı testi Pest ile yazmak şöyle görünür:

<?php

use App\Models\User;

uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);

it('profil güncellenebiliyor', function () {
    $user = User::factory()->create();

    $this->actingAs($user)
        ->put('/profile', ['name' => 'Yeni İsim'])
        ->assertRedirect('/profile');

    $this->assertDatabaseHas('users', [
        'id' => $user->id,
        'name' => 'Yeni İsim',
    ]);
});

it() fonksiyonu testi tanımlar, uses() ise trait'leri tüm dosyaya uygular. Pest'te expect() API'si de yaygındır:

expect($user->name)->toBe('Yeni İsim');

Pest ve PHPUnit arasında seçim yaparken şunu unutmayın: ikisi de aynı altyapıyı kullandığı için performans farkı beklemeyin. Seçim büyük ölçüde ekip tercihidir. Mevcut bir PHPUnit paketini tamamen Pest'e çevirmek zorunlu değildir; ikisi bir arada çalışabilir.

Testleri çalıştırmak

php artisan test

Bu komut test paketini çalıştırır ve okunaklı bir çıktı verir. Belirli bir dosyayı veya testi çalıştırmak için:

php artisan test --filter=UserRegistrationTest
php artisan test tests/Feature/UserProfileTest.php

Pest kullanıyorsanız ./vendor/bin/pest de kullanılabilir. Yoğun geliştirme sırasında değişen dosyalara göre test çalıştıran filtreleme seçenekleri zaman kazandırır.

Yaygın hatalar

  • Test ortamını izole etmemek: Üretim veya geliştirme veritabanına karşı test çalıştırmak. Her zaman ayrı bir test bağlantısı tanımlayın.
  • Aşırı mock'lamak: Neredeyse her bağımlılığı mock'layan testler, gerçek entegrasyonu doğrulamaz. Mock'u yalnızca dış servisler veya yavaş/pahalı işlemler için kullanın.
  • Testleri birbirine bağımlı yazmak: Bir testin sonucu diğerinin çalışmış olmasına bağlıysa paket kırılgan olur. Her test kendi verisini kurmalı.
  • Sadece mutlu yolu test etmek: Doğrulama hataları, yetkisiz erişim ve boş girdi gibi senaryoları da test edin.
  • Rastgele/zamana bağlı veri: Sabit tarih veya rastgele değerler testleri kararsız (flaky) yapar. Laravel'in zaman dondurma yardımcılarıyla zamanı sabitleyin.

Güvenlik açısından testin rolü

Testler güvenlik garantisi değildir ama önemli bir güvenlik ağıdır. Yetkilendirme kurallarını test etmek, bir kullanıcının başkasının kaynağına erişemediğini doğrulamak açısından değerlidir:

public function test_baska_kullanicinin_kaydina_erisilemiyor(): void
{
    $owner = User::factory()->create();
    $other = User::factory()->create();
    $post = Post::factory()->for($owner)->create();

    $this->actingAs($other)
        ->get("/posts/{$post->id}")
        ->assertStatus(403);
}

Beklenen davranış: Yetkilendirme politikası doğruysa 403 döner. Policy'de bir hata varsa test bunu yakalar. Aynı mantıkla CSRF koruması, rate limiting ve girdi doğrulaması gibi savunma katmanlarını da test edebilirsiniz. Ancak unutmayın: test edilmemiş her yol potansiyel bir açıktır ve testler yalnızca yazdığınız senaryoları kapsar.

CI'ya bağlamak

Testleri sürekli entegrasyon hattına eklemek, hataların erken yakalanmasını sağlar. Tipik bir adım, bağımlılıkları kurup test paketini çalıştırmaktır:

composer install --no-interaction --prefer-dist
php artisan test

CI ortamında da test veritabanını ayrı tanımladığınızdan emin olun. Testler yerelde geçip CI'da kırılıyorsa ilk bakılacak yer genellikle ortam değişkenleri ve veritabanı yapılandırmasıdır.

Kapanış

PHPUnit ve Pest aynı temelin iki yüzüdür; hangisini seçerseniz seçin, belirleyici olan testlerin izole, okunabilir ve gerçek davranışı doğruluyor olmasıdır. Küçük başlayın: en kritik iş akışları için birkaç feature testi yazın, veritabanını izole edin ve testleri CI'ya bağlayın. Test paketi büyüdükçe unit testleriyle hızı, feature testleriyle kapsamı dengede tutun.

İLGİLİ İÇERİKLER

Bunları da Okumak İsteyebilirsiniz