İçeriğe geç

Test verisi yönetimi: fixture, factory ve üretim verisi kopyalamanın maliyeti

Regresyon setini çökerten şey test değil, veri

Ortak bir veritabanı üzerinde koşan bir regresyon setinde kırmızıya dönen testlerin çoğu, test ettikleri kodda bir hata olduğu için kırmızı değildir. Beklenen veri durumu bulunamadığı, başka bir testin aynı kaydı değiştirdiği veya bir önceki koşumdan artakalan bir nesne benzersizlik kısıtını ihlal ettiği için kırmızıdır. Bunu ayırt etmek zaman alır, çünkü hata mesajı “kullanıcı bulunamadı” der; “başka bir test bu kullanıcıyı sildi” demez.

Problemin adı paylaşılan ve kontrolsüz durumdur. İki test aynı satıra dokunduğunda aralarında bir bağımlılık doğar, ama bu bağımlılık hiçbir dosyada yazılı değildir. Kod incelemesinde görünmez; import edilmez, çağrılmaz, tip sisteminden geçmez. Yalnızca çalışma sırası değiştiğinde veya testler paralelleştiğinde ortaya çıkar. Tipik görüntüsü şudur: set tek iş parçacığında ve alfabetik sırayla yeşil, iki worker’la kırmızı. Ekip bunu “flaky test” diye etiketler, retry sayısını artırır ve asıl sorunu bir sonraki sprinte erteler. Kararsız testin kök nedenini bulma yazısında anlattığım teşhis adımlarının büyük kısmı zaten bu noktaya, veri durumuna çıkar.

Test verisinin üç kaynağı ve her birinin gerçek maliyeti

Test verisi üç yerden gelir: depoya kaydedilmiş sabit fixture dosyaları, çalışma anında üretilen factory’ler ve üretimden kopyalanıp maskelenen veri setleri.

Sabit fixture en ucuz kurulumdur: bir JSON dosyası yazarsınız, biter. Maliyeti kurulumda değil, bakımda ortaya çıkar. Şema değiştiğinde dosyayı kimse güncellemez, çünkü derleyici uyarmaz. İki test aynı fixture kaydını değiştirmeye kalktığında çakışma kesindir. Gizlilik riski sıfırdır.

Factory kurulumda pahalıdır: her varlık için bir üretici fonksiyon, benzersizlik stratejisi ve temizleme mekanizması yazmanız gerekir. Karşılığında kırılganlık düşer, çünkü her test kendi verisini üretir ve kimseyle paylaşmaz. Şema değişimi tek bir dosyada, factory’nin varsayılanlarında karşılanır. Gizlilik riski sıfırdır.

Üretim kopyası kurulumda en pahalısıdır ve maliyeti hiç bitmez: anonimleştirme boru hattı, yenileme takvimi, erişim kontrolü ve yasal sorumluluk. Kırılganlığı yüksektir, çünkü testin dayandığı kayıt bir sonraki yenilemede silinmiş olabilir. Gizlilik riski en yüksek olanıdır.

Seviyelere dağılımı şöyle kurulur: birim testler veriyi bellekte, elle oluşturur. Entegrasyon ve API testleri factory kullanır. Uçtan uca testler de factory kullanır, ön koşulları API’den kurar. Üretim kopyası yalnızca performans ve veri göçü testlerine aittir — fonksiyonel regresyona değil.

Sabit fixture ne zaman doğru karardır

Fixture, referans verisi için doğru araçtır. Ülke ve il listeleri, para birimleri, vergi oranları, ürün kategorileri, rol ve yetki tanımları, kargo firmaları. Bu verinin ortak noktası şudur: testin konusu değildir, ortamın bir parçasıdır. Hiçbir test bu kayıtları değiştirmez, hepsi yalnızca okur. Paylaşılan ama değişmeyen durum tehlikeli değildir; tehlike değişebilirlikten gelir.

Referans verisini uygulamanın kendi migration mekanizmasıyla yüklemek, ayrı bir test seed dosyasında tutmaktan iyidir. Böylece test ortamındaki referans verisi üretimdekiyle aynı kaynaktan gelir ve ikisi ayrışmaz.

İşlem verisi — siparişler, müşteriler, faturalar, başvurular — fixture’a girmemelidir. musteri_1 diye sabit bir kayıt tanımladığınız anda, o kaydı kullanan her test diğerlerinin ne yaptığına bağımlı hale gelir. Biri müşteriyi pasife çeker, diğeri aktif bekler. Bu çakışma olasılık değil, zamanla gerçekleşecek bir kesinliktir.

Factory deseni: her test kendi verisini üretir

Factory’nin işi, bir varlığın zorunlu alanlarını makul varsayılanlarla doldurup testin yalnızca ilgilendiği alanı override etmesine izin vermektir. Böylece testte yalnızca anlamlı olan bilgi görünür: “kurumsal segmentte bir müşteri” cümlesi testin ilk satırında okunur, yirmi alanlık bir nesne değil.

Benzersizlik stratejisi olarak çalışma kimliği ile sayacın birleşimi kullanılır. Yalnızca rastgele değer üretmek de çalışır ama hata ayıklamayı zorlaştırır; çalışma kimliği taşıyan kayıtlar, koşum sonrası veritabanında tek sorguyla bulunur.

// tests/factories/ids.ts
import { randomBytes } from 'node:crypto';

const RUN_ID = process.env.TEST_RUN_ID ?? randomBytes(4).toString('hex');
let counter = 0;

export function uniqueSuffix(): string {
  counter += 1;
  return `${RUN_ID}-${counter.toString().padStart(4, '0')}`;
}

export function uniqueEmail(prefix = 'musteri'): string {
  return `${prefix}+${uniqueSuffix()}@test.example.com`;
}
// tests/factories/customer.ts
import type { APIRequestContext } from '@playwright/test';
import { uniqueEmail, uniqueSuffix } from './ids';

export type Customer = {
  id: string;
  email: string;
  fullName: string;
  segment: 'bireysel' | 'kurumsal';
  creditLimit: number;
  active: boolean;
};

export type CustomerInput = Partial<Omit<Customer, 'id'>>;

export function buildCustomer(overrides: CustomerInput = {}): Omit<Customer, 'id'> {
  const suffix = uniqueSuffix();
  return {
    email: uniqueEmail(),
    fullName: `Test Musteri ${suffix}`,
    segment: 'bireysel',
    creditLimit: 10_000,
    active: true,
    ...overrides,
  };
}

export async function createCustomer(
  api: APIRequestContext,
  overrides: CustomerInput = {},
): Promise<Customer> {
  const payload = buildCustomer(overrides);
  const response = await api.post('/api/v1/customers', { data: payload });

  if (!response.ok()) {
    throw new Error(
      `Musteri olusturulamadi (${response.status()}): ${await response.text()}`,
    );
  }

  const body = (await response.json()) as { id: string };
  return { id: body.id, ...payload };
}

Kurulumu API üzerinden yapmanın gerekçesi, veritabanına doğrudan yazmanın uygulama katmanındaki iş kurallarını atlamasıdır. Elle INSERT ettiğiniz bir müşteri, uygulamanın asla üretemeyeceği bir durumda olabilir — eksik bir varsayılan, tetiklenmemiş bir olay, oluşmamış bir denetim kaydı. Test o zaman gerçekte var olmayan bir senaryoyu doğrular. Doğrudan veritabanı yazımı, yalnızca API’nin üretemediği durumlar (arşivlenmiş kayıt, geçmiş tarihli veri) için kaçış kapısı olarak kalmalıdır.

Kurulumu UI’dan değil API’den yapmak

Bir sipariş iptali senaryosunu test etmek için arayüzden ürün seçip sepete ekleyip ödeme akışını tamamlamak, testi iki şekilde bozar. Birincisi süre: on beş saniyelik kurulum, doğrulanan davranışın kendisinden uzun sürer. İkincisi ve daha kötüsü sorumluluk karmaşası: ödeme sayfasındaki bir değişiklik, iptal testini kırar. Test raporunda “iptal çalışmıyor” yazar, oysa iptal çalışmaktadır.

Kural nettir: ön koşul API’den kurulur, yalnızca doğrulanan davranış arayüzden geçer. Playwright’ta bunu fixture katmanına gömmek, her testte tekrar etmekten temizdir.

// tests/fixtures.ts
import { test as base, type APIRequestContext } from '@playwright/test';
import { createCustomer, type Customer, type CustomerInput } from './factories/customer';

type DataFactory = {
  customer: (overrides?: CustomerInput) => Promise<Customer>;
};

type Fixtures = {
  api: APIRequestContext;
  data: DataFactory;
};

export const test = base.extend<Fixtures>({
  api: async ({ playwright }, use) => {
    const context = await playwright.request.newContext({
      baseURL: process.env.API_BASE_URL,
      extraHTTPHeaders: { Authorization: `Bearer ${process.env.API_TOKEN}` },
    });
    await use(context);
    await context.dispose();
  },

  data: async ({ api }, use) => {
    const created: Array<{ path: string; id: string }> = [];

    await use({
      customer: async (overrides = {}) => {
        const customer = await createCustomer(api, overrides);
        created.push({ path: '/api/v1/customers', id: customer.id });
        return customer;
      },
    });

    for (const item of created.reverse()) {
      const response = await api.delete(`${item.path}/${item.id}`);
      if (!response.ok() && response.status() !== 404) {
        console.warn(`Temizlenemedi: ${item.path}/${item.id} (${response.status()})`);
      }
    }
  },
});

export { expect } from '@playwright/test';
// tests/customer-list.spec.ts
import { test, expect } from './fixtures';

test('kurumsal musteri listede segment rozetiyle gorunur', async ({ page, data }) => {
  const customer = await data.customer({ segment: 'kurumsal' });

  await page.goto('/musteriler');
  await page.getByRole('searchbox', { name: 'Müşteri ara' }).fill(customer.email);

  const row = page.getByRole('row', { name: customer.email });
  await expect(row).toBeVisible();
  await expect(row.getByTestId('segment-rozeti')).toHaveText('Kurumsal');
});

Testin tek bir override’ı var: segment. Geri kalan her şey factory’nin sorumluluğunda ve testin niyeti ilk satırda okunuyor. Temizleme, fixture teardown’ında ters sırada çalışıyor; test gövdesinde tek satır temizleme kodu yok.

Temizleme stratejisi: silme, geri alma ve izole şema

Dört yaklaşım var ve seçim, uygulamanın mimarisine bağlı.

Ters sırada silme yukarıdaki örnekteki yöntem. Yabancı anahtar kısıtları yüzünden oluşturma sırasının tersi gerekir. Avantajı her yerde çalışması; dezavantajı silme API’sinin her varlık için var olmaması ve soft delete kullanan sistemlerde kaydın gerçekte kalmasıyla benzersizlik kısıtlarının ihlal edilmeye devam etmesi.

İşlemi geri alma, testi bir transaction içinde çalıştırıp sonunda rollback etmektir. Hızlıdır ve eksiksizdir, ama yalnızca test kodu ile uygulama kodu aynı bağlantıyı paylaşıyorsa mümkündür. Uçtan uca testte tarayıcı ayrı bir HTTP isteği yaptığı için bu koşul sağlanmaz; entegrasyon ve depo katmanı testlerinde uygulanabilir.

Her çalıştırma için ayrı şema veya tenant, temizleme problemini ortadan kaldırır: koşum sonunda şemayı düşürürsünüz. En temiz yöntem ve paralelleştirmeyle en uyumlu olanı. Bedeli, uygulamanın çok kiracılı olması veya bağlantı dizesinin çalışma anında değiştirilebilmesi gerekliliğidir.

Hiç temizlememek de geçerli bir stratejidir ve hafife alınmamalıdır. Benzersiz alan değerleri kullanıyorsanız artan kayıtlar kimseyi rahatsız etmez; test ortamı veritabanını haftalık olarak sıfırlarsınız. Bu yaklaşım temizleme kodunun kendi hatalarını (yarım kalan silme, yanlış sıra, timeout) ortadan kaldırır. Sakıncası, zamanla büyüyen tabloların liste sayfalarını yavaşlatması ve “toplam kayıt sayısı” gibi kümülatif doğrulamaları imkânsız kılmasıdır — ki o tür doğrulamalar zaten ortak veritabanında yapılmamalıdır.

Üretim verisi kopyalamanın faturası

Üretim kopyası cazip görünür: gerçek veri, gerçek hacim, gerçek uç durumlar. Faturası dört kalemde gelir.

Birincisi yasal sorumluluk. KVKK ve GDPR açısından test ortamındaki kişisel veri, üretimdekiyle aynı korumaya tabidir. Test ortamının erişim kontrolü, log politikası ve saklama süresi üretim seviyesine çıkarılmalıdır. Pratikte test ortamlarına geliştirici, danışman ve bazen tedarikçi erişir; bu, veri işleme envanterine girmesi gereken bir durumdur.

İkincisi anonimleştirme boru hattının kendisinin bir yazılım olması. Yazılırken bitmez: her yeni tablo, her yeni kişisel veri alanı, her serbest metin sütunu boru hattına eklenmelidir. Tipik olarak gözden kaçan yer, yapılandırılmamış alanlardır — açıklama notları, ekler, denetim kayıtları ve e-posta gövdeleri. Bir alanı maskelemeyi unutmanın maliyeti, boru hattını hiç kurmamış olmakla aynıdır.

Üçüncüsü yenileme sıklığı ile test kararlılığı arasındaki çelişki. Kopyayı sık yenilerseniz testlerin dayandığı kayıtlar değişir ve set kırılır. Seyrek yenilerseniz veri üretim gerçekliğini yansıtmayı bırakır ve kopyalamanın tek gerekçesi ortadan kalkar. Bu çelişkinin çözümü yoktur; yalnızca bir taraf seçilir.

Dördüncüsü şema değişimi. Kopya, alındığı andaki şemaya aittir. Göç betikleri kopya üzerinde de çalıştırılmalıdır; çalıştırılmadığında kopya sessizce eskir.

Üretim kopyasının gerçekten gerekli olduğu iki yer var: performans testleri (gerçekçi hacim ve veri dağılımı olmadan anlamsızdır) ve veri göçü testleri (göçün amacı zaten mevcut veriyi taşımaktır). Fonksiyonel regresyon için gerekli değildir.

Anonimleştirme yeterli değil, sentetik üretim gerekli

Maskeleme kimlik çıkarımını tamamen engellemez. Ad ve TC kimlik numarasını değiştirseniz bile, doğum tarihi + posta kodu + işlem geçmişi üçlüsü küçük popülasyonlarda tek bir kişiye indirgenebilir. Bu, kayıt birleştirme saldırısının bilinen bir biçimidir ve “kişisel alanları yıldızladık” cümlesiyle kapanmaz.

Karşı hamle olarak maskelemeyi agresifleştirdiğinizde ikinci problem başlar: veri ilişkileri bozulur. Müşteri şehrini rastgele değiştirirseniz kargo ücreti hesaplaması anlamsız girdiyle çalışır. Tarihleri kaydırırsanız yaş kontrolleri, kampanya geçerlilikleri ve yaşlandırma raporları tutarsız olur. Testin gerçekçiliği için kopyalanan veri, gerçekçiliğini maskeleme yüzünden kaybeder.

Alternatif, üretim verisinin istatistiksel profilini çıkarıp aynı dağılıma sahip sentetik veri üretmektir: sipariş tutarlarının dağılımı, müşteri başına sipariş sayısı, kategori payları, boş alan oranları. Bu profil kişisel veri içermez ve depoda tutulabilir. Üretim ise profilden yapılır.

Bu yaklaşımın ana zorluğu referans bütünlüğüdür. Tek bir tabloyu dağılıma uygun doldurmak kolaydır; on beş tabloyu birbirine tutarlı yabancı anahtarlarla, gerçekçi kardinalitelerle ve geçerli durum makinesi geçişleriyle doldurmak zordur. Sipariş, iptal edilmiş bir müşteriye ait olamaz; iade, teslim edilmemiş bir siparişe bağlanamaz. Pratikte en sağlam yöntem, sentetik veriyi doğrudan tablolara yazmak yerine uygulamanın kendi API’si üzerinden üretmektir — bütünlüğü uygulama zaten zorunlu kılar. Mantığın dış sistemlere dayandığı yerlerde mocking ve stubbing bu üretimi tamamlar.

Ortak veritabanından paralel çalıştırmaya geçiş

Paralelleştirme bir yapılandırma ayarı değil, veri izolasyonunun sonucudur. workers: 4 yazmak, izolasyon yoksa yalnızca hataları hızlandırır.

TEST_RUN_ID taşıyan CI koşusu üç worker'a dallanır; her biri kendi kiracı/şemasına, hepsi ortak veritabanına bağlanır.

Üç izolasyon seçeneği var. Kiracı bazlı ayrım, uygulama zaten çok kiracılıysa en ucuz yoldur: her worker kendi kiracısında çalışır, sorgular zaten kiracı kimliğiyle filtrelenir. Kullanıcı bazlı ayrım, kiracı kavramı yoksa uygulanır; her worker kendi kullanıcısıyla oturum açar ve yalnızca kendi ürettiği kayıtlara dokunur. Zayıf noktası, yönetici ekranları ve global listeler gibi kullanıcıdan bağımsız görünümlerdir — bu testler izole edilemez ve seri çalıştırılmalıdır. Şema bazlı ayrım en güçlüsüdür ve global görünümler dahil her şeyi izole eder, ama bağlantı yönlendirmesi ile şema oluşturma/düşürme altyapısı ister.

Worker sayısı, izolasyon biriminin sayısıyla sınırlıdır: üç kiracınız varsa dördüncü worker’ın izole edecek yeri yoktur. Bu yüzden izolasyon birimi, koşum başında dinamik olarak oluşturulabilir olmalıdır. Ölçekleme tarafındaki diğer kısıtlar için test otomasyonunda ölçeklenebilirlik yazısına bakabilirsiniz.

Veri sahipliğini süreçle sabitlemek

Teknik çözüm kurulduktan sonra çürümesinin nedeni, sahipliğin tanımsız kalmasıdır. Üç soru yazılı olarak cevaplanmalı: test verisi altyapısı kimin sorumluluğunda, factory’leri kim günceller, şema değiştiğinde fixture’ları kim düzeltir.

İşleyen model, factory’lerin varlığı geliştiren ekibin sorumluluğunda olmasıdır. Müşteri varlığına yeni bir zorunlu alan ekleyen kişi, müşteri factory’sinin varsayılanını da aynı pull request’te ekler. QA ekibi factory’nin tüketicisidir, bakımcısı değil — aksi halde factory’ler her zaman şemanın bir adım gerisinde kalır.

Pull request kontrol listesine giren maddeler:

  • Yeni veya değişen zorunlu alan için ilgili factory güncellendi mi?
  • Yeni test sabit bir kayıt kimliği veya sabit e-posta kullanıyor mu? Kullanıyorsa gerekçesi ne?
  • Testin ürettiği kayıtlar teardown’da temizleniyor mu veya benzersiz alan kullanıyor mu?
  • Ön koşul arayüzden mi kuruluyor? Öyleyse API’ye taşınabilir mi?
  • Test, başka bir testin ürettiği veriye bağımlı mı?

Yeni test yazan kişinin uyacağı kural seti tek cümleye indirgenebilir: testin ihtiyaç duyduğu her değişebilir kaydı test kendisi üretir, referans verisi dışında hiçbir şeyi hazır bulmaz. Her testin kendi verisini üretmesinin neden zorunlu olduğunu ayrı bir yazıda daha ayrıntılı ele almıştım.

Bundan sonra ne yapmalı

Sırayla ilerleyin, dördünü aynı anda başlatmayın.

1. Son 30 günün başarısız test kayıtlarını etiketleyin. Her başarısızlık için tek bir alan ekleyin: veri kaynaklı mı, değil mi. Elle yapılabilecek bir iştir ve bir günden uzun sürmez. Çıkan oran, bu yazıdaki hangi bölümden başlayacağınızı söyler. Oran düşükse veri sorununuz yoktur; enerjinizi başka yere harcayın.

2. En çok paylaşılan üç veri nesnesi için factory yazın. Hangi nesnelerin en çok paylaşıldığını, testlerde tekrar eden sabit kimlikleri arayarak bulursunuz. Üçten fazlasıyla başlamayın; desen oturmadan yaygınlaştırmak, yanlış deseni yaygınlaştırmak demektir.

3. Üretim kopyası kullanıyorsanız iki şeyi yazıya dökün: kopyanın yenilenme sıklığı ve kopyada bulunan kişisel veri alanlarının listesi. Bu iki belge, kopyayı sürdürmenin mi yoksa sentetik üretime geçmenin mi daha ucuz olduğunu tartışabilmeniz için gereken asgari girdidir. Liste çıkarılamıyorsa cevap zaten bellidir.

4. Tek bir test dosyasını paralel çalışacak şekilde izole edin. Tüm seti değil, bir dosyayı. Hangi izolasyon biriminin mimarinize uyduğunu ancak bir kez uygulayınca anlarsınız. Çalıştığında aynı deseni ikinci dosyaya taşıyın.

API kurulumunu bu akışın merkezine alacaksanız, API test otomasyonunun nasıl kurulduğu üzerinden bir tur atmak ikinci adımı hızlandırır.

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.