İçeriğe geç

Test verisi yönetimi: Her testin kendi verisini üretmesi neden zorunlu

Test paketiniz tek başına çalışırken yeşil, CI’da paralel koşarken kırmızıysa sorun genellikle testlerde değil, testlerin paylaştığı veridedir. beforeAll içinde kurulan bir kullanıcı, migration ile yüklenen bir seed kaydı, “zaten var” varsayımıyla kullanılan bir sipariş numarası — bunların hepsi testler arasında görünmez bir bağımlılık yaratır.

Bu yazı tek bir kural etrafında dönüyor: bir testin okuduğu veya değiştirdiği her kaydı o test üretmiş olmalı. Kuralı nasıl uygulayacağınızı, kaçınılmaz istisnalarını ve mevcut bir paketi bu kurala nasıl taşıyacağınızı anlatıyorum.

Sorun paylaşılan test verisinde başlar

Ortak seed verisi kullanan bir paket, testler arasında yazılı olmayan bir sözleşme kurar: “kimse user_42‘nin e-postasını değiştirmeyecek”. Bu sözleşme derleyici tarafından kontrol edilmez, code review’da görünmez ve testler alfabetik sırayla çalıştığı sürece bozulmaz. Sonra biri yeni bir test dosyası ekler, dosya adı sıralamayı değiştirir ve profil güncelleme testi, listeleme testinin beklediği kaydı bozar.

Bu noktada olan şey şudur: listeleme testi kırmızı yanar, hata mesajı “beklenen 3 satır, bulunan 2 satır” der ve ekip bunu ürün hatası sanıp saatlerce arar. Testi tek başına çalıştırınca geçer. Buna “flaky test” deyip retries: 2 eklemek, teşhisi yanlış koymaktır. Flaky test, aynı girdiyle farklı sonuç veren testtir. Burada girdi farklıdır — veritabanının durumu, önceki testin ne yaptığına bağlıdır. Bu bir zamanlama sorunu değil, mimari bir hatadır: testler arasında paylaşılan değiştirilebilir durum var.

Retry eklemek bu durumda işleri kötüleştirir. Yeniden denenen test, ilk denemede kendi bıraktığı artık veriyi bulur ve bu sefer başka bir nedenle düşer. Ya da tesadüfen geçer ve gerçek bir regresyonu maskeler.

Bu ayrımı yapmak için üç kontrol yeter. Testi tek başına çalıştır: geçiyorsa sorun testte değil izolasyonda. Sırayı değiştir (--shuffle veya dosya adını değiştir): sonuç değişiyorsa testler arasında bağımlılık var. Aynı testi arka arkaya iki kez koştur: ikincisi düşüyorsa test kendi bıraktığı artığı buluyor. Üçü de temiz geçiyorsa gerçekten bir zamanlama sorunun olabilir; geçmiyorsa retries eklemek teşhisi geciktirmekten başka bir işe yaramaz.

Test verisi sahipliği kuralı

Kural tek cümle: bir testin okuduğu veya değiştirdiği her kaydı o test üretmiş olmalı.

Bu kuralın üç doğrudan sonucu var:

  1. Testler sırasız çalışabilir. Hiçbir test, başka bir testin bıraktığı duruma bağlı değildir. Dosya adını değiştirmek, yeni test eklemek veya --shuffle ile çalıştırmak sonucu etkilemez.
  2. Testler paralel çalışabilir. İki worker aynı kaydı aynı anda güncelleyemez, çünkü kayıtlar ayrıdır.
  3. Testler tek başına çalışabilir. Bir hatayı ayıklarken tek testi -g ile çalıştırdığınızda, 40 testlik bir setup zincirini yeniden kurmanız gerekmez. Bu, hata ayıklama süresini doğrudan kısaltır.

Kuralın tek meşru istisnası referans verisidir: ülke listesi, para birimleri, KDV oranları, ürün kategorileri, rol tanımları. Bunlar istisnadır çünkü testler bu veriyi yalnızca okur ve hiçbir test onu değiştirmez. Değiştirilmeyen veri, testler arasında bağımlılık yaratmaz.

İstisnanın sınırı da net: bir test referans veriyi değiştirmek zorundaysa (yeni bir para birimi ekleme testi gibi), o test kendi kaydını üretmeli ve silmeli. “Sadece bu bir tane, nasılsa geri alırım” diyerek ortak listeye dokunan test, kuralın dışında değildir — kuralı ihlal eder.

Kuralı uygulamaya geçirmek

Sahiplik kuralı üç mekanizma ister: testin kendi kaydını üreten bir factory, ön koşulu arayüz yerine API’den kuran bir kurulum adımı, ve koşum sonrası ürettiğini geri alan bir temizleme stratejisi. Üçünün de nasıl kurulduğunu ve hangisinin hangi ortamda doğru olduğunu, her birinin gerçek maliyetiyle birlikte ayrı bir yazıda ele aldım: Test verisi yönetimi: fixture, factory ve üretim verisi kopyalamanın maliyeti.

Bu yazının geri kalanı kuralın iki zor köşesini anlatıyor: veriyi değiştiremediğiniz ortamlar, ve paralel çalıştırma.

Değiştirilemeyen veriyle çalışmak: üretim benzeri ortamlar

Bazı sistemlerde silme yoktur. Muhasebe kayıtları, denetim izi (audit log), ödeme hareketleri, resmî belge numaraları — bunlar tasarım gereği değiştirilemez. Bazen de veri sizin sisteminizde değil, entegre olduğunuz dış bir sistemde üretilir ve orada silme yetkiniz yoktur.

Bu durumda izolasyonu silmeyle sağlayamazsınız; kapsam ayırmayla sağlarsınız. Pratik seçenekler:

  • Test başına ayrı tenant veya organizasyon. Kayıt kalır ama başka hiçbir testin sorgu kapsamına girmez.
  • Test başına ayrı hesap. Muhasebe sistemlerinde tipik çözüm: her test kendi cari hesabını açar, hareketleri o hesaba yazar, bakiyeyi o hesap üzerinden doğrular.
  • Ayrı tarih aralığı. Raporlama testlerinde işe yarar: her test kendi ürettiği kayıtları kendi seçtiği bir tarih penceresine yazar ve raporu o pencereyle filtreler.
  • Ayrı etiket/referans alanı. Kayda externalRef gibi bir alanda uniqueTag yazıp bütün sorguları bu alanla filtrelemek.

Bu yaklaşımın sınırlarını da bilin. Sistem genelinde tekil olan kaynaklar (fatura numarası sayacı, global stok miktarı, gün sonu toplamı) kapsam ayırmayla izole edilemez. Bu tür davranışları test eden senaryolar gerçekten seri çalışmalıdır; Playwright’ta bunları ayrı bir projeye alıp fullyParallel: false ile çalıştırmak, tüm paketi seri çalıştırmaktan iyidir. İkinci sınır: veri birikir. Üretim benzeri ortamda aylar sonra tablo boyutu sorgu performansını etkiler, bu yüzden yaş bazlı bir arşivleme işi yine gerekir.

Test verisi ve paralel çalıştırma ilişkisi

Paralel çalıştırma yeni bir özellik değil, mevcut veri mimarinizin sınavıdır. workers: 1 ile çalışan bir paket veri sahipliği hakkında hiçbir şey kanıtlamaz; sıra sabit olduğu için ihlaller görünmez. Worker sayısını 4’e çıkardığınızda düşen her test, veri sahipliğinin ihlal edildiği yeri işaret eder. Düşen testlerin listesi, yapılacaklar listenizdir.

Burada iki fixture kapsamı arasındaki farkı doğru kurmak gerekir:

  • Test-scoped veri: testin değiştirdiği her şey. Kullanıcılar, siparişler, ürünler. Her test kendi kaydını üretir.
  • Worker-scoped veri: kurulumu pahalı, testin değiştirmediği ve yalnızca okuduğu şeyler. Bir tenant, bir admin oturumu, bir yapılandırma. Worker başına bir kez kurulur, o worker’daki tüm testler paylaşır.

Ayrım basit: testin değiştirdiği hiçbir şey worker-scoped olmamalıdır. Bir test worker-scoped tenant’ın ayarını değiştirirse, aynı worker’daki sonraki testler bozulur — paylaşılan seed verisinin küçük ölçekli hâline geri dönmüş olursunuz.

// fixtures/tenant.ts
import { test as base, expect, request, APIRequestContext } from '@playwright/test';

export type Tenant = { id: string; slug: string; adminToken: string };

type WorkerFixtures = { tenant: Tenant };
type TestFixtures = { tenantApi: APIRequestContext };

export const test = base.extend<TestFixtures, WorkerFixtures>({
  tenant: [
    async ({}, use, workerInfo) => {
      const admin = await request.newContext({
        baseURL: process.env.API_URL ?? 'http://localhost:3000',
        extraHTTPHeaders: { Authorization: `Bearer ${process.env.ADMIN_TOKEN ?? ''}` },
      });

      const runId = process.env.TEST_RUN_ID ?? String(Date.now());
      const slug = `qa-${runId}-w${workerInfo.workerIndex}`;

      const response = await admin.post('/api/tenants', { data: { slug, plan: 'standard' } });
      expect(response.status(), await response.text()).toBe(201);
      const body = await response.json();

      await use({ id: body.id, slug, adminToken: body.adminToken });

      await admin.delete(`/api/tenants/${body.id}`).catch(() => undefined);
      await admin.dispose();
    },
    { scope: 'worker' },
  ],

  tenantApi: async ({ tenant, playwright }, use) => {
    const context = await playwright.request.newContext({
      baseURL: process.env.API_URL ?? 'http://localhost:3000',
      extraHTTPHeaders: {
        Authorization: `Bearer ${tenant.adminToken}`,
        'X-Tenant': tenant.slug,
      },
    });
    await use(context);
    await context.dispose();
  },
});

export { expect };

Bu yapıda tenant kurulumu worker başına bir kez ödenir, testler kendi kayıtlarını o tenant içinde üretir ve iki worker asla aynı veri kümesine bakmaz. Paketi büyütürken kapsam kararlarının nasıl değiştiğine dair daha geniş bir çerçeve için ölçeklenebilirlik yazısına bakabilirsiniz.

Worker sayısını artırırken iki sayı birlikte anlam kazanır: toplam süre ve düşen test sayısı. Süre worker sayısıyla orantılı kısalmıyorsa darboğaz testlerde değil, paylaşılan bir kaynaktadır — çoğunlukla veritabanı bağlantı havuzu ya da worker başına tekrarlanan tek bir kurulum adımı. Düşen test sayısı artıyorsa izolasyon eksiktir; hangi testlerin düştüğüne bak, aynı tabloya dokunanlar bir arada çıkar. İkisini aynı anda değiştirme: önce worker sayısını sabit tutup izolasyonu düzelt, sonra artır. Aksi hâlde hangi değişikliğin neyi düzelttiğini bilemezsin.

Sözleşme ve mock sınırında test verisi

Mocklanan bir bağımlılık için yazdığınız sahte yanıt da test verisidir ve aynı sahiplik kuralına tabidir. fixtures/payment-response.json dosyasını on iki test paylaşıyorsa, o dosya paylaşılan veritabanı tablosuyla aynı problemi üretir: biri “tutarı 250 yapayım” dediğinde dokuz test düşer ve düşen testlerin hiçbiri ödeme tutarını doğrulamıyordur.

Çözüm aynı: mock yanıtını da factory ile üretin. Temel bir nesne döndüren bir fonksiyon yazın, test kendi ihtiyacı olan alanı override etsin. Mock ve stub arasındaki ayrımı ve nerede hangisinin uygun olduğunu mocking ve stubbing yazısında ele almıştım.

İkinci sorun daha sinsi: mock verisi gerçek şemadan sapabilir. Sağlayıcı alan adını değiştirir, sizin mock’unuz eski adı döndürmeye devam eder, testler yeşil kalır ve üretimde patlar. Bunun tek gerçek çözümü mock verisini sözleşme testine bağlamaktır — kullandığınız sahte yanıtların sağlayıcının yayımladığı şemaya karşı doğrulanması. Mock fixture’larınızı elle yazılmış JSON dosyaları olarak bırakmak yerine, sözleşme doğrulamasından geçen tek bir kaynaktan üretin.

Mevcut paketi kademeli olarak taşımak

Çalışan bir paketi baştan yazmanız gerekmez ve yazmamalısınız; taşıma sırası bellidir.

1. Ölçüm yapın. Paketi --workers=2 ve --shuffle ile çalıştırın, düşen testleri kaydedin. Bu liste, veri sahipliğinin ihlal edildiği yerlerin haritasıdır. Sıralamayı değiştirmeden ölçmek yanıltıcıdır.

2. Dokunulan tabloları çıkarın. Düşen testlerin hangi kayıtları okuduğunu ve hangilerini değiştirdiğini yazın. Genellikle bir avuç tablo (kullanıcı, hesap, ürün) sorunların çoğundan sorumlu çıkar.

3. Önce yazma yapan testleri izole edin. Kirliliği üretenler onlardır. Bu testlere factory fixture’ı verin, seed kaydına dokunmayı bırakın. Bu adımın kendisi, okuma yapan testlerin bir kısmını hiçbir değişiklik yapmadan düzeltir.

4. Sonra okuma yapan testleri taşıyın. Artık kimse seed’i bozmuyor olsa da, “listede en az 3 kayıt var” tarzı assert’ler kırılgandır. Bu testleri kendi ürettikleri kayıtları arayacak şekilde yazın: sayı değil kimlik doğrulayın.

5. Her adımda worker sayısını artırın. 2 worker yeşilse 4’e çıkın. İlerlemeyi “kod daha temiz göründü” ile değil, “8 worker ile paket yeşil” ile ölçün. Bu, taşımanın bittiğini anlamanın tek objektif yoludur.

Bu sırayı bozmayın: temizlik altyapısını kurmadan worker sayısını artırmak, ortamı çöple doldurup ekibin güvenini kaybettirir.

Bundan sonra ne yapmalı

Bu hafta yapabilecekleriniz:

  1. Paketi --workers=2 --shuffle ile çalıştırın ve düşen testlerin listesini bir dosyaya yazın. Düzeltmeyin, sadece listeleyin — bu sizin başlangıç ölçümünüz.
  2. Listenin başındaki testin dokunduğu ana kaydı seçin ve o kayıt için tek bir factory fixture’ı yazın. Yalnızca bir test dosyasını bu fixture’a taşıyın.
  3. Benzersiz kimlik üretimini uniqueTag mantığıyla standartlaştırın ve mevcut Math.random() kullanımlarını arayıp değiştirin.
  4. Temizliği fixture teardown’ına alın ve best-effort hâle getirin; hiçbir temizlik hatası testi kırmasın.
  5. Ortama, qa+ önekli ve 24 saatten eski kayıtları silen zamanlanmış bir iş ekleyin. Bu iş yoksa diğer dört madde zamanla erir.
  6. CI’da worker sayısını bir kademe artırın ve 1. maddedeki listeyle karşılaştırın.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

Bu site istenmeyenleri azaltmak için Akismet kullanır. Yorum verilerinizin nasıl işlendiğini öğrenin.