Test setiniz yerel olarak başarılı. Ancak CI’da başarısız oluyor.
Pipeline’ı yeniden çalıştırıyorsunuz. Şimdi her şey yeşil.
Hiçbir şey değiştirmediniz. Bir saat sonra, rastgele başka bir hata ortaya çıkıyor.
Bu durum size tanıdık geliyorsa, muhtemelen “flaky tests” ile karşı karşıyasınız.
Flaky testler, bazı zamanlarda başarılı, bazı zamanlarda başarısız olan ve anlamlı kod değişiklikleri olmadan oluşan testlerdir. Modern yazılım geliştirme süreçlerindeki en stresli sorunlardan biridir çünkü test setinize olan güveni yavaşça yok eder.
Geliştiriciler testlere güvenmeyi bıraktığında, hataları görmezden gelir, pipeline’ları körü körüne yeniden çalıştırır ve nihayetinde üretime hatalı kod gönderirler.
Birçok Laravel projesinde flaky testlerle uğraştıktan sonra önemli bir şey fark ettim:
Çoğu flaky test, PHPUnit’in kendisinden kaynaklanmıyor.
Genellikle gizli paylaşılan durumu, zaman varsayımlarını, asenkron davranışı veya testler arasında sızan altyapıyı temsil eder.
Bu makalede, Laravel’de flaky testlerin en yaygın nedenlerini ve bunları nasıl doğru bir şekilde düzeltebileceğinizi göstereceğim.
Bir Testin “Flaky” Olmasının Nedenleri
Flaky bir testin üç temel özelliği vardır:
- İnconsistent olarak başarısız olur
- Başarısızlık, yeniden üretimi zor olan bir durumdur
- Testi yeniden çalıştırmak genellikle “onarır”
Bu durum, normal bir başarısız testten farklıdır.
Normal bir başarısız test, belirleyici bir hatayı gösterir.
Flaky bir test ise belirsizlik yaratır.
Ve belirsizlik, CI alt yapısında tehlikelidir çünkü geliştiriciler zamanla hataları ciddiye almayı bırakır.
Flaky testlerin en yaygın kaynaklarından biri zamanla ilgilidir.
Laravel, Carbon ile zamanla çalışmayı kolaylaştırır, ancak zaman temelli mantık kolayca kararsız hale gelebilir.
Bu örneği düşünün:
public function test_subscription_expires_after_24_hours(): void
{
$subscription = Subscription::factory()->create([
'expires_at' => now()->addDay(),
]);
sleep(1);
$this->assertFalse($subscription->isExpired());
}
Bu test çoğu zaman başarılı olabilir.
Ancak şunlara bağlı olarak:
- CI hızı
- Sunucu yükü
- İşlem zamanlaması
- Zaman dilimi yönetimi
Sonuçta, tahmin edilemez bir şekilde başarısız olabilir.
Çözümü basit:
Sabah zaman kullanın.
Carbon::setTestNow('2026-05-28 10:00:00');
$subscription = Subscription::factory()->create([
'expires_at' => now()->addDay(),
]);
$this->assertFalse($subscription->isExpired());
Ve her zaman sonrası temizlemeyi yapın:
Carbon::setTestNow();
Temizlik yapılmadığında, sahte zaman başka testlere sızabilir ve daha fazla rastgelelik yaratabilir.
Flaky testlerin bir diğer büyük kaynağı ise testler arasında veritabanı sızıntısıdır.
Hâlâ bazı projelerde, testler önceki testler tarafından oluşturulan kayıtlara bağlıdır.
Örnek:
public function test_user_can_create_post(): void
{
$this->post('/posts', [
'title' => 'Example',
]);
$this->assertDatabaseCount('posts', 1);
}
Bu başlangıçta zararsız görünse de, başka bir test veritabanına post eklediğinde, sayım aniden 2, 5 veya hatta 12 olabilir.
Çözüm ise düzgün veritabanı izolasyonudur.
Laravel’de bu genellikle:
use RefreshDatabase;
veya:
use DatabaseTransactions;
mimarinize bağlı olarak gereklidir.
Fabrikalardan büyük yarar sağlamaktayız.
Ancak rastgelelik sıkıntılı bir durumu ifade eder.
Bu test masum görünüyor:
$user = User::factory()->create();
$this->assertEquals('admin', $user->role);
Ancak fabrika rastgele roller oluşturursa, bu test hemen kararsız hale gelir.
Bunu özellikle büyük Laravel projelerinde görmekteyiz, çünkü fabrikalar yıllar içinde evrim geçirerek rastgeleliği her yere yaymışlardır.
Bunun yerine, gerekli durumu açıkça tanımlayın:
$user = User::factory()->create([
'role' => 'admin',
]);
Deterministik veriler, deterministik testler oluşturur.
Queue’lar en büyük flaky davranış kaynaklarından biridir.
Özellikle geliştiriciler bazı işleri asenkron olarak çalıştırmaya devam ederken, kuyrukları kısmen sahteleme yaptıklarında.
Örnek:
Queue::fake();
dispatch(new SendInvoiceJob($invoice));
$this->assertDatabaseHas('invoices', [
'status' => 'sent',
]);
Bu, kuyruklu işin aslında çalışmaması nedeniyle başarısız olabilir.
Daha kötüsü: durum, çevre yapılandırmasına bağlı olarak bazen çalışabilir.
Diğer bir yaygın sorun, asenkron görevler işlenmeden hemen sonra işleme davranışını test etmektir.
Örnek:
dispatch(new SyncProductsJob());
$this->assertDatabaseCount('products', 500);
Doğrulama, işçi tamamlanmadan önce çalıştırılabilir.
Yerel olarak geçer. CI’da rastgele başarısız olur.
Daha iyi bir yaklaşım ya:
- kendi başına kuyruklamayı test etmek,
- veya testlerde işlerin senkron bir şekilde çalıştırılmasıdır.
Örnek:
Bus::fake();
dispatch(new SyncProductsJob());
Bus::assertDispatched(SyncProductsJob::class);
Ya da:
config()->set('queue.default', 'sync');
test ortamında.
Paralel testler CI’yı büyük ölçüde hızlandırır.
Ancak aynı zamanda gizli paylaşılan durumları ortaya çıkarır.
Paylaşılan Redis anahtarları, paylaşılan dosyalar, önbelleğe alınmış yapılandırmalar, geçici dizinler, statik değişkenler ve singleton durumu yüzünden hatalar oluşabilir.
Örnek:
Storage::disk('local')->put('report.pdf', 'content');
Birden fazla test aynı dosyaya aynı anda yazarsa, rastgele hatalar ortaya çıkar.
Çözüm izolasyon sağlamaktır.
Örnek:
Storage::fake();
veya unique dosya isimleri:
$file = Str::uuid() . '.pdf';
Paralel testler flaky testler yaratmaz.
Mevcut olan sorunları açığa çıkarır.
Testler içinde gerçek HTTP çağrıları yapmak tehlikelidir.
Bazen API yavaş olabilir.
Bazen oran sınırları tetiklenebilir.
Bazen sandbox ortamları çalışmayabilir.
Ve birdenbire test setiniz, uygulamanızdan tamamen bağımsız nedenlerle güvenilmez hale gelebilir.
Bu yüzden dış API’lar genelde sahte ya da kurgusal olmalıdır.
Laravel, mükemmel HTTP sahteleme sunar:
Http::fake([
'*' => Http::response([
'success' => true,
], 200),
]);
Artık testleriniz:
- daha hızlıdır
- deterministiktir
- network güvenilirliğinden bağımsızdır
Bu konu hakkında API sahteleme makalemde daha fazla bilgi verdim, çünkü dış entegrasyonlar, zorla kararsız testler üretmenin en kolay yollarından biridir.
Bu durum son derece tehlikelidir.
Bir test yalnızca, başka bir testin önce çalışmasına bağlı olarak geçmektedir.
Örnek:
public function test_admin_exists(): void
{
$this->assertDatabaseHas('users', [
'email' => '[email protected]',
]);
}
Bu, başka bir testin önce admin kullanıcısını yaratmasına bağımlıdır.
Testleri bireysel olarak çalıştırdığınızda, bu aniden başarısız olur.
İyi bir test şunları yapmalıdır:
- bağımsız
- tekrar edilebilir
- herhangi bir sırada çalışabilir
Eğer yürütme sırası önemliyse, test seti kırılgandır.
Flaky testlerin en büyük sorunu teknik değildir.
Psikolojik bir sorundur.
Geliştiriciler CI’ya güvenmeyi bıraktığında:
- hatalar göz ardı edilir
- tekrar çalıştırmalar normal hale gelir
- gerçek hatalar gözden kaçar
- güven kaybolur
CI’nın her zaman flaky olduğunu düşünen takımlarda, geliştiriciler birden fazla kez pipeline’ları yeniden çalıştırmaktadır.
Bu tehlikelidir.
Çünkü en nihayetinde gerçek bir regresyon gürültü içinde gizlenebilir.
Flaky testler nadiren rastgele oluşur.
Hemen hemen her zaman bir mühendislik problemi vardır:
- paylaşılan durum
- kontrolsüz zaman
- asenkron davranış
- izole edilmemiş altyapı
- gizli bağımlılıklar
Çözüm “CI’yı yeniden çalıştırmak” değildir.
Çözüm, testlerin deterministik hale getirilmesidir.
Güvenilir bir test seti her zaman aynı sonuca ulaşmalıdır:
- yerelde
- CI’da
- her makinada
- her yürütme sırasındaki durumlarda
Testleriniz deterministik hale geldiğinde, tüm geliştirme iş akışınız daha hızlı, daha güvenli ve çok daha az sinir bozucu hale gelir.
Kaynak: Orijinal Makale


