Modern Yazılım Test Stratejileri: Birim Testten Uçtan Uca Teste
Hataları gerçekten yakalayan ancak yavaşlatmayan bir test stratejisi oluşturmak için pratik bir rehber — birim testleri, entegrasyon testleri, Playwright ile E2E, TDD, özellik tabanlı test, görsel regresyon, mock kalıpları ve test trophy ile piramit tartışmasını kapsar.
Her yazılım ekibi test yazar. Ancak her ekip, hataları önleyen, yeniden düzenlemelere dayanan ve ekibe Cuma öğleden sonra dağıtım yapma güveni veren testler yazmaz. Yardımcı olan testlerle zarar veren testler arasındaki fark, test çerçevesi veya kapsama yüzdesi değildir. Fark stratejidedir — neyi, hangi seviyede ve hangi ödünle test edeceğini bilmektir.
Birçok ekip için varsayılan yaklaşım, her şey için birim testleri yazıp günü kurtarmaktır. Kapsama raporları yüzde 90'ı gösterir, CI geçer ve bir fonksiyondaki değişiklik, hiçbir birim testinin yakalamadığı üç özelliği bozana kadar herkes iyi hisseder. Sorun test disiplini eksikliği değil, yanlış seviyede test yapmaktır. Beş servisi birbirine bağlayan bir bileşen yüzde 100 birim test kapsamına sahip olabilir ve yine de üretimde başarısız olabilir, çünkü bu servisler arasındaki etkileşim hiçbir zaman doğrulanmamıştır.
Bu rehber, hızlı izole birim testlerden yavaş ama güvenilir uçtan uca testlere kadar modern test stratejilerinin tam spektrumunu kapsar ve her stratejinin ne zaman değer kattığını, ne zaman gereksiz yük oluşturduğunu açıklar. Amaç sizi daha fazla test yazmaya ikna etmek değil, var olmayı hak eden — önemli hataları yakalayan, sık çalıştırılabilecek kadar hızlı olan ve kod tabanınızın kaçınılmaz yeniden yapılandırmalarına dayanan — testler yazmanıza yardımcı olmaktır.
Birim testte en iyi uygulamalar ve ne test edilmeli
Birim testleri, hızlı, deterministik ve izole oldukları için çoğu test stratejisinin temelidir. İyi yazılmış bir birim testi milisaniyeler içinde çalışır, tek bir mantıksal davranışı kapsar ve veritabanları, dosya sistemleri veya ağ API'leri gibi harici sistemlere bağımlı değildir. Bir birim testi başarısız olduğunda, hangi mantık parçasının bozuk olduğunu ve nedenini tam olarak bilirsiniz.
Birim testinde en yaygın hata, davranış yerine uygulama detaylarını test etmektir. Bir fonksiyonun belirli bir iç metodu çağırdığını veya belirli bir özel alanı ayarladığını test etmek, testi kırılgan yapar — dış davranışı koruyan bir yeniden düzenleme testi bozacaktır. Test, güvenlik ağı olmaktan çıkıp bir yükümlülük haline gelir. Bir fonksiyonun gözlemlenebilir çıktısı veya yan etkileri üzerine iddialar yazın, fonksiyonun bu sonuca dahili olarak nasıl ulaştığına değil.
Birim testi için iyi adaylar arasında saf yardımcı fonksiyonlar, G/Ç içermeyen iş mantığı, doğrulama ve ayrıştırma mantığı, veri dönüşümleri, algoritmik hesaplamalar ve durum makineleri bulunur. Kötü adaylar arasında ağ çağrıları yapan fonksiyonlar, UI render eden bileşenler ve çerçeve iç yapılarına sıkı sıkıya bağlı kodlar yer alır. Bunlar entegrasyon veya E2E seviyesinde daha iyi test edilir.
import { describe, it, expect } from "vitest";
import { calculateDiscount, type Order } from "./pricing";
describe("calculateDiscount", () => {
it("returns 0 for orders below the threshold", () => {
const order: Order = {
items: [{ price: 30, quantity: 1 }],
customerSince: new Date("2025-01-01"),
};
expect(calculateDiscount(order)).toBe(0);
});
it("applies 10 percent for orders over $100", () => {
const order: Order = {
items: [
{ price: 80, quantity: 1 },
{ price: 40, quantity: 1 },
],
customerSince: new Date("2025-01-01"),
};
expect(calculateDiscount(order)).toBe(12);
});
it("applies loyalty bonus for customers over 2 years", () => {
const order: Order = {
items: [{ price: 100, quantity: 1 }],
customerSince: new Date("2022-06-01"),
};
expect(calculateDiscount(order)).toBe(15);
});
it("does not apply discount when items are on sale", () => {
const order: Order = {
items: [{ price: 200, quantity: 1, onSale: true }],
customerSince: new Date("2025-01-01"),
};
expect(calculateDiscount(order)).toBe(0);
});
});Yukarıdaki örnek, tek bir fonksiyonun farklı girdiler ve beklenen çıktılarla dört ayrı davranışını test eder. Her test bağımsız, deterministik ve 10 milisaniyenin altında çalışır. Fiyatlandırma mantığı değişirse, testler hangi senaryoların etkilendiğini tam olarak söyler. Birim testinin değer önermesi budur: saf mantık üzerinde hızlı geri bildirim.
- Davranışları test edin, uygulamayı değil. Çıktılar ve yan etkiler üzerine iddialar yazın, iç metot çağrıları veya özel durum üzerine değil.
- Test başına tek bir mantıksal iddia. Bir test tam olarak tek bir nedenden başarısız olmalı, böylece neyin bozulduğu açıkça bellidir.
- Cümle gibi okunan açıklayıcı test adları kullanın. "returns 0 for orders below the threshold", "test_discount_low_amount"dan daha iyidir.
- Testler arasında paylaşılan değişebilir durumdan kaçının. Her test kendi verisini kurmalı ve sonrasında temizlemelidir.
- Test değerlerini basit ve anlamlı tutun. Gerçek alan nesnelerini yansıtan gerçekçi veriler kullanın.
Gerçek bağımlılıklarla entegrasyon testi
Entegrasyon testleri, hız ve kapsam açısından birim testleri ile E2E testleri arasında yer alır. Birden çok birimin birlikte nasıl çalıştığını test ederler — gerçek bir veritabanını çağıran bir repository, gerçek bir API uç noktasıyla konuşan bir servis veya gerçek bir mağaza ile render eden bir bileşen. Birim testlerinden temel farkı, entegrasyon testlerinin mock'lar yerine gerçek veya konteynerize bağımlılıklar kullanmasıdır.
En değerli entegrasyon testleri, kodunuzun harici sistemlerle doğru şekilde etkileşime girdiğini doğrular. Veritabanı katmanını mock'layan bir birim testi, sorgu mantığınızın izole olarak doğru olup olmadığını söyleyebilir, ancak gerçek SQL sorgusunun gerçek şemaya karşı doğru çalışıp çalışmadığını söyleyemez. Testcontainers veya test veritabanı kullanan entegrasyon testleri, şema uyumsuzluklarını, kısıtlama ihlallerini, işlem sınırı hatalarını ve birim testlerinin tamamen kaçırdığı serileştirme sorunlarını yakalar.
Entegrasyon testleri, repository katmanları, API rota işleyicileri, mesaj kuyruğu tüketicileri, veritabanı migrasyonları ve sınır boyunca veri serileştiren/deserileştiren her türlü kod için idealdir. Ödünleşim hızdır — bir Postgres konteyneri başlatan test saniyeler yerine milisaniyeler alır — ancak yakaladıkları hataları üretimde bulmanın maliyeti orantısal olarak çok daha yüksektir.
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { createDatabase, teardownDatabase } from "./test-utils";
import { OrderRepository } from "./order-repository";
describe("OrderRepository", () => {
let db: Awaited<ReturnType<typeof createDatabase>>;
let repo: OrderRepository;
beforeAll(async () => {
db = await createDatabase();
repo = new OrderRepository(db);
});
afterAll(async () => {
await teardownDatabase(db);
});
it("persists and retrieves an order", async () => {
const order = {
id: "ord_001",
customerId: "cus_001",
total: 1250,
status: "pending" as const,
createdAt: new Date("2025-06-01"),
};
await repo.save(order);
const retrieved = await repo.findById("ord_001");
expect(retrieved).toEqual(order);
});
it("returns null for a non-existent order", async () => {
const result = await repo.findById("ord_nonexistent");
expect(result).toBeNull();
});
it("updates order status", async () => {
await repo.save({
id: "ord_002",
customerId: "cus_001",
total: 500,
status: "pending",
createdAt: new Date(),
});
await repo.updateStatus("ord_002", "shipped");
const updated = await repo.findById("ord_002");
expect(updated?.status).toBe("shipped");
});
});Entegrasyon testleri için kritik kural, veritabanı durumunu test çalıştırmaları arasında izole tutmaktır. Her test takımı kendi şemasını veya veritabanını oluşturmalı, migrasyonları çalıştırmalı, testleri yürütmeli ve her şeyi temizlemelidir. Paylaşılan bir veritabanına karşı test çalıştırmak, bir testin verisinin diğerinin iddialarına sızdığı tutarsız testlere yol açar. Her test çalıştırmasının bilinen bir durumdan başladığından emin olmak için Testcontainers, Docker Compose veya çerçevenizin yerleşik test veritabanı desteğini kullanın.
Gerçek veritabanıyla yapılan bir entegrasyon testi, veritabanını mock'layan yüz birim testine bedeldir. Mock size veritabanından ne beklediğinizi söyler. Gerçek veritabanı size gerçekte ne yaptığını söyler. İkisi, ilk şema migrasyonundan sonra nadiren aynıdır.
Playwright ile uçtan uca test
Uçtan uca testler, tüm uygulama yığını boyunca — tarayıcı, ön yüz, API, veritabanı ve harici servisler — gerçek kullanıcı etkileşimlerini simüle eder. En yavaş ve en maliyetli test türüdür, ancak en gerçekçi hataları yakalar: bozuk navigasyon, eksik form doğrulaması, kullanıcıya gösterilen yanlış API yanıtları, kimlik doğrulama akışı hataları ve çapraz tarayıcı render sorunları.
Playwright, çapraz tarayıcı desteği, otomatik beklemeli iddialar, ağ müdahalesi ve geliştirici deneyimi sayesinde web uygulamaları için baskın E2E test aracı haline gelmiştir. Açık beklemeler ve yeniden denemeler gerektiren eski araçların aksine, Playwright öğelerle etkileşime geçmeden önce onların etkileşime hazır olmasını bekler, bu da tutarsız testlerin en yaygın kaynağını ortadan kaldırır.
import { test, expect } from "@playwright/test";
test("user completes a purchase flow", async ({ page }) => {
await page.goto("/products");
await page.getByRole("link", { name: "Wireless Headphones" }).click();
await page.getByRole("button", { name: "Add to Cart" }).click();
await page.getByRole("button", { name: "View Cart" }).click();
await expect(page.getByText("Wireless Headphones")).toBeVisible();
await expect(page.getByText("$89.99")).toBeVisible();
await page.getByRole("button", { name: "Checkout" }).click();
await page.getByLabel("Email").fill("test@example.com");
await page.getByLabel("Card Number").fill("4242424242424242");
await page.getByLabel("Expiry").fill("12/28");
await page.getByLabel("CVC").fill("123");
await page.getByRole("button", { name: "Pay Now" }).click();
await expect(page.getByText("Order confirmed")).toBeVisible();
await expect(page.getByText("ord_001")).toBeVisible();
});Ekiplerin E2E testleriyle yaptığı en büyük hata çok fazla test yazmaktır. Her E2E testi CI hattınıza dakikalar ekler ve 200 E2E testlik bir takımın çalışması kolayca 30 dakika sürebilir. E2E testin ekonomisi seçicilik gerektirir. Kritik kullanıcı yolculuklarına — kayıt, giriş, satın alma, arama, ödeme — odaklanın ve alt seviye testlerin uç durumları ve hata durumlarını kapsamasına izin verin.
- Gelir açısından kritik akışlar için E2E testlerine öncelik verin: ödeme, abonelik yükseltmeleri, ödeme işleme.
- E2E testlerini bağımsız tutun. Her test kendi verisini oluşturmalı ve sonrasında temizlemelidir.
- Test durumuna daha hızlı ulaşmak için UI etkileşimleri yerine test kurulumunda (beforeAll) API çağrıları kullanın.
- E2E testlerini staging veya önizleme ortamında çalıştırın, üretimde değil. Test verisi görünürlüğünü kontrol etmek için özellik bayrakları kullanın.
- Test gözlemlenebilirliğine yatırım yapın — başarısız testlerin ekran görüntüleri, izleri ve video kayıtları saatlerce hata ayıklama süresinden tasarruf ettirir.
Pratikte test güdümlü geliştirme
Test güdümlü geliştirme test yazmakla ilgili değildir. Tasarımla ilgilidir. TDD döngüsü — kırmızı, yeşil, yeniden düzenleme — sizi kodu uygulamadan önce kodunuzun arayüzü hakkında düşünmeye zorlar. Önce testi yazmak şu soruyu yanıtlamanızı sağlar: bu fonksiyon ne kabul etmeli ve ne döndürmeli? Bu kısıtlama daha iyi API'lere, daha gevşek bağlantıya ve daha modüler koda yol açar.
TDD'ye direnç genellikle iki yerden birinden gelir. Ya geliştirici yaklaşıma uygun olmayan bir problemde (UI kodu, keşif çalışması) denemiştir ya da dogmatik bir şekilde uygulamış ve faydalı kod yazmaktansa test çerçevesiyle savaşarak daha fazla zaman harcamıştır. TDD bir araçtır, bir din değil. Algoritmik mantık, veri dönüşümleri, API tasarımı ve girdi-çıktı sözleşmesinin yazmaya başlamadan önce net olduğu her türlü kod için en iyi şekilde çalışır.
Pratik bir TDD iş akışı şöyle görünür: uygulamak istediğiniz bir sonraki davranışı tanımlayan tek bir test yazın. Testi çalıştırın ve başarısız olmasını izleyin — bu, testin geçerli olduğunu ve özelliğin yokluğunu tespit edebildiğini onaylar. Testi geçirecek minimum kodu yazın. Çözümü aşırı mühendislikle yapmayın; testi karşılayan en basit uygulama doğru olandır. Ardından, davranışını değiştirmeden kodun yapısını iyileştirmek için yeniden düzenleme yapın ve geçen testlere güvenerek gerilemeleri yakalayın.
TDD sizi daha fazla test yazmaya itmez. Daha iyi kod yazmanızı sağlar. Test, tasarım sürecinin bir yan ürünüdür, hedef değil. Testleri uygulamadan sonra yazıyorsanız, test ediyorsunuzdur. Testleri uygulamadan önce yazıyorsanız, tasarlıyorsunuzdur.
TDD, hata düzeltmeleri için özellikle etkilidir. Bir hata raporlandığında, hatayı yeniden üreten bir test yazın (kırmızı), kodu düzeltin (yeşil) ve düzeltmenin mevcut davranışı bozmadığını doğrulayın (tüm testler hala geçiyor). Yazdığınız test kalıcı bir regresyon koruması haline gelir ve hatanın bir daha ortaya çıkmamasını sağlar. Zamanla bu, kod tabanının gerçek hata geçmişini yansıtan bir test takımı oluşturur ki bu, bir kapsama hedefini karşılamak için oluşturulmuş bir takımdan çok daha değerlidir.
Özellik tabanlı ve fuzz testi
Geleneksel örnek tabanlı testler, belirli girdileri beklenen çıktılara karşı kontrol eder. Özellik tabanlı test farklı bir yaklaşım benimser: tüm girdiler için geçerli olması gereken bir özellik tanımlar ve bu özelliği doğrulamak için yüzlerce veya binlerce rastgele girdi üretir. Bu teknik, hiçbir insanın test örneği olarak yazmayı düşünmeyeceği uç durumları yakalar.
Örneğin, bir sıralama fonksiyonu için beş özel test durumu yazmak yerine, bir özellik tanımlarsınız: karşılaştırılabilir öğelerden oluşan herhangi bir liste için, sıralanmış sonuç aynı öğeleri azalmayan sırada içermelidir. Test çerçevesi, değişen boyutlarda, tekrarlarla, boş listelerle ve uç değerlerle rastgele listeler oluşturur ve özelliğin her biri için geçerli olduğunu doğrular.
import { describe, it, expect } from "vitest";
import { faker } from "@faker-js/faker";
describe("sortByPrice", () => {
it("returns items in ascending price order for any input", () => {
for (let i = 0; i < 100; i++) {
const items = Array.from(
{ length: faker.number.int({ min: 0, max: 50 }) },
() => ({
name: faker.commerce.productName(),
price: faker.number.int({ min: 1, max: 1000 }),
})
);
const sorted = sortByPrice(items);
for (let j = 1; j < sorted.length; j++) {
expect(sorted[j - 1].price).toBeLessThanOrEqual(sorted[j].price);
}
}
});
it("preserves all original elements", () => {
for (let i = 0; i < 100; i++) {
const items = Array.from(
{ length: faker.number.int({ min: 0, max: 50 }) },
() => ({
name: faker.commerce.productName(),
price: faker.number.int({ min: 1, max: 1000 }),
})
);
const sorted = sortByPrice(items);
expect(sorted).toHaveLength(items.length);
expect(sorted.map((i) => i.name).sort()).toEqual(
items.map((i) => i.name).sort()
);
}
});
});Fast-check, TypeScript için Vitest ve Jest ile entegre olan popüler bir özellik tabanlı test kütüphanesidir. Yaygın veri türleri için üreteçler, karmaşık yapılar için birleştiriciler ve otomatik küçültme sağlar — başarısız bir test durumu bulunduğunda, kütüphane onu mümkün olan en küçük başarısız girdiye indirgemeye çalışır, bu da hata ayıklamayı önemli ölçüde kolaylaştırır.
Özellik tabanlı test, özellikle serileştirme ve deserileştirme mantığı, sıralama ve filtreleme algoritmaları, doğrulama kuralları, durum makinesi geçişleri ve olası girdi uzayının büyük olduğu her türlü fonksiyon için değerlidir. Sisteminize bozuk veya beklenmedik veriler besleyen fuzz test teknikleriyle birleştirildiğinde, özellik tabanlı testler aksi takdirde yalnızca üretimde keşfedilecek güvenlik açıklarını ve çökme hatalarını ortaya çıkarır.
Görsel regresyon testi
Fonksiyonel testler, uygulamanın doğru çalıştığını doğrular. Görsel regresyon testleri, doğru göründüğünü doğrular. CSS değişiklikleri, bağımlılık güncellemeleri ve yeniden düzenlenen bileşenler, hiçbir fonksiyonel testin yakalamadığı ince yerleşim kaymalarına, renk uyuşmazlıklarına, yazı tipi değişikliklerine veya duyarlı kesme noktası sorunlarına neden olabilir. Görsel regresyon testi, bileşenlerin veya sayfaların ekran görüntülerini alır ve bunları temel görüntülerle karşılaştırarak insan incelemesi için görsel farklılıkları işaretler.
Chromatic, Percy ve Playwright'ın yerleşik ekran görüntüsü yetenekleri gibi araçlar, görsel regresyon testini farklı seviyelerde uygular. Chromatic, Storybook ile entegre olur ve kenar yumuşatma ve alt piksel render farklılıklarını yok sayan akıllı fark bulma ile piksel bazında karşılaştırma sağlar. Percy, daha geniş çerçeve desteğiyle benzer yetenekler sunar. Playwright'ın toHaveScreenshot iddiası, ek bir araç gerektirmeden doğrudan E2E testlerinizde görsel iddialar yazmanıza olanak tanır.
import { test, expect } from "@playwright/test";
test("product page renders correctly", async ({ page }) => {
await page.goto("/products/wireless-headphones");
await expect(page).toHaveScreenshot("product-page.png", {
maxDiffPixelRatio: 0.01,
threshold: 0.2,
fullPage: true,
});
});
test("checkout form is visually stable across states", async ({ page }) => {
await page.goto("/checkout");
await expect(page).toHaveScreenshot("checkout-empty.png");
await page.getByLabel("Email").fill("invalid-email");
await page.getByRole("button", { name: "Continue" }).click();
await expect(page).toHaveScreenshot("checkout-validation-error.png");
});Görsel regresyon testinin ana zorluğu, temel görüntüleri yönetmektir. Her kasıtlı görsel değişiklik, temel görüntülerin güncellenmesini gerektirir ve inceleme süreci, sık sık UI değişikliği yapan ekipler için darboğaz haline gelebilir. Çözüm, temel güncellemelerini geliştirme iş akışının bir parçası olarak ele almaktır — görsel farklılıkları, kod değişiklikleriyle aynı inceleme döngüsünde, çekme isteği iş akışınızla entegre bir görsel inceleme platformu kullanarak onaylayın.
- Kritik sayfalarla başlayın: açılış sayfası, ödeme, giriş, ürün detayları ve karmaşık düzenlere sahip diğer sayfalar.
- İşletim sistemleri arasındaki kenar yumuşatma yapaylıklarını ve yazı tipi render farklılıklarını yok saymak için uygun eşik değerleri ayarlayın.
- Her değişikliğe duyarlı tam sayfa ekran görüntüleri yerine, hedefli görsel kapsama için bileşen seviyesinde ekran görüntüleri (Storybook aracılığıyla) kullanın.
- Görsel incelemeyi PR iş akışınıza entegre edin. Değişen sayfalardaki görsel farklılıklar için insan onayı gerektirin.
- Geliştirici makineleri arasındaki render farklılıklarını ortadan kaldırmak için görsel testleri tutarlı bir ortamda (Docker) çalıştırın.
React Sunucu Bileşenlerini ve Next.js'i test etme
React Sunucu Bileşenleri, React uygulamaları için test ortamını değiştirdi. Sunucu Bileşenleri yalnızca sunucuda çalışır ve istemciye sıfır JavaScript gönderir. Bu mantığın hiçbirini tarayıcıya maruz bırakmadan doğrudan veritabanlarına, dosya sistemlerine ve arka uç servislere erişebilirler. Bu da onları istemci tarafı bileşenlerden farklı bir yaklaşımla test edilebilir kılar.
Sunucu Bileşenleri aslında JSX döndüren asenkron fonksiyonlardır. Onları herhangi bir asenkron fonksiyon gibi test edebilirsiniz — prop'larla çağırın, sonucu bekleyin ve render edilen çıktı üzerinde iddialar yapın. Tarayıcı ortamı, fetch veya veritabanı çağrılarını mock'lama gerekmez. Bu basitlik, Sunucu Bileşenlerinin genellikle takdir edilmeyen faydalarından biridir: test kurulumu, bir DOM ortamı gerektiren istemci bileşenlerini test etmekten çok daha basittir.
import { describe, it, expect } from "vitest";
import { ProductList } from "./product-list";
describe("ProductList (Server Component)", () => {
it("renders products from the database", async () => {
// No mocking needed — the component calls the database directly
const { props } = await ProductList({ category: "electronics" });
expect(props.products.length).toBeGreaterThan(0);
expect(props.products.every((p) => p.category === "electronics")).toBe(
true
);
});
it("shows empty state when no products match", async () => {
const { props } = await ProductList({ category: "nonexistent" });
expect(props.products).toHaveLength(0);
});
});useState, useEffect, tarayıcı API'leri veya olay işleyicileri kullanan istemci bileşenleri bir DOM test ortamına ihtiyaç duyar. Vitest, bir tarayıcıyı simüle eden happy-dom veya jsdom ortamı sağlar. Testing Library, render edilen çıktıyı sorgulamak ve kullanıcı etkileşimlerini simüle etmek için yardımcı programlar sunar. Sunucu Bileşen testinden temel fark, bileşeni render etmeniz, efektlerin çalışmasını beklemeniz ve ortaya çıkan DOM durumu üzerinde iddialarda bulunmanız gerektmesidir.
Next.js uygulamaları, ek test hususları gerektiren yönlendirme, veri getirme ve sunucu eylemleri ekler. getServerSideProps veya generateStaticParams kullanan sayfa bileşenleri için, veri getirme fonksiyonlarını sayfa bileşeninden bağımsız olarak test edin. Sunucu eylemleri için, onları normal asenkron fonksiyonlar olarak test edin — onlar sadece sunucuda çalışan fonksiyonlardır. App Router düzenleri ve sayfaları için, veri katmanını sunum katmanından ayrı olarak test ederek testlerinizi çerçevenin render iç yapılarına bağlamaktan kaçının.
import { describe, it, expect } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { CheckoutForm } from "./checkout-form";
describe("CheckoutForm", () => {
it("shows validation errors for empty required fields", async () => {
const user = userEvent.setup();
render(<CheckoutForm />);
await user.click(screen.getByRole("button", { name: "Submit" }));
expect(screen.getByText("Email is required")).toBeVisible();
expect(screen.getByText("Card number is required")).toBeVisible();
});
it("submits the form with valid data", async () => {
const onSubmit = vi.fn();
const user = userEvent.setup();
render(<CheckoutForm onSubmit={onSubmit} />);
await user.type(screen.getByLabelText("Email"), "test@example.com");
await user.type(screen.getByLabelText("Card Number"), "4242424242424242");
await user.type(screen.getByLabelText("Expiry"), "12/28");
await user.type(screen.getByLabelText("CVC"), "123");
await user.click(screen.getByRole("button", { name: "Submit" }));
expect(onSubmit).toHaveBeenCalledWith({
email: "test@example.com",
cardNumber: "4242424242424242",
expiry: "12/28",
cvc: "123",
});
});
});Yeni React modelindeki en önemli ayrım, Sunucu Bileşen testlerini İstemci Bileşen testlerinden ayırmaktır. Sunucu Bileşenleri saf asenkron fonksiyonlardır — onları DOM ortamı olmadan test edin. İstemci Bileşenleri kullanıcı etkileşimi simülasyonuna ihtiyaç duyar — onları Testing Library ve userEvent ile test edin. Bu iki endişeyi karıştırmak, testlerinizi gereğinden yavaş ve karmaşık hale getirir.
Mock stratejileri ve moklardan ne zaman kaçınılmalı
Mock'lama, test yazımında en çok tartışılan konulardan biridir. Mock'lar, gerçek bağımlılıkları kontrollü ikamelerle değiştirerek, veritabanları kurmadan, harici API'leri çağırmadan veya zamanlayıcıları beklemeden kodu izole bir şekilde test etmenize olanak tanır. Ödünleşim, mock'ların kodunuzu bağımlılığın gerçek değil, simüle edilmiş bir sürümüne karşı doğrulamasıdır. Mock, gerçek davranışı doğru şekilde yansıtmıyorsa, test geçerken gerçek kod başarısız olur.
Genel kılavuz, sisteminizin sınırında mock yapmaktır. Kontrol etmediğiniz harici servisleri (üçüncü taraf API'ler, ödeme işlemcileri, e-posta servisleri) ve testlere dahil edilmesi pahalı veya deterministik olmayan altyapıyı (veritabanları, dosya sistemleri, saatler) mock'layın. İç modülleri, yardımcı fonksiyonları veya kendi alan nesnelerinizi mock'lamayın — bunlar entegrasyon testlerinde gerçek uygulamaları aracılığıyla test edilmelidir.
Vitest, mock bildirimlerini dosyanın üst kısmına kaldıran ve testler çalışmadan önce modül uygulamalarını değiştiren bir vi.mock sistemi sağlar. Bu, ortam değişkenlerini, üçüncü taraf SDK'ları ve global nesneleri mock'lamak için kullanışlıdır. Risk, modül seviyesinde mock'lamanın görünmez olması ve birlikte çalıştırıldığında mock etkileşimi nedeniyle başarısız olan ancak izole halde geçen testlere yol açabilmesidir.
import { describe, it, expect, vi } from "vitest";
import { PaymentService } from "./payment-service";
const mockStripe = {
charges: {
create: vi.fn(),
},
};
vi.mock("stripe", () => ({
default: vi.fn(() => mockStripe),
}));
describe("PaymentService", () => {
it("creates a charge successfully", async () => {
mockStripe.charges.create.mockResolvedValue({
id: "ch_001",
status: "succeeded",
});
const service = new PaymentService();
const result = await service.charge(5000, "tok_visa");
expect(result.status).toBe("succeeded");
expect(mockStripe.charges.create).toHaveBeenCalledWith({
amount: 5000,
currency: "usd",
source: "tok_visa",
});
});
it("throws on failed payment", async () => {
mockStripe.charges.create.mockRejectedValue(
new Error("card_declined")
);
const service = new PaymentService();
await expect(service.charge(5000, "tok_visa")).rejects.toThrow(
"card_declined"
);
});
});Mock'lama yerine daha güvenilir bir alternatif, gerçek bağımlılıkla aynı arayüzü uygulayan ancak basitleştirilmiş bir bellek içi uygulamaya sahip test ikizleri kullanmaktır. Örneğin, veritabanı modülünü mock'lamak yerine, verileri bir Map'te depolayan bir repository'nin bellek içi sürümünü oluşturun. Bu test ikizi, kurulum maliyeti olmadan gerçek veritabanıyla aynı kod yollarını çalıştıran hafif ama sadık bir uygulama görevi görür.
Her mock, testlerinizle gerçeklik arasında bir boşluk yaratır. Bu boşluk, hataların saklandığı yerdir. Bir mock'a uzanmadan önce sorun: bunu kontrollü bir ortamda gerçek bağımlılıkla test edebilir miyim? Cevap evetse, gerçek olanı kullanın. Cevap hayırsa — bağımlılık çok yavaş, çok pahalı veya deterministik değil — o zaman mock yapın, ancak sistemi sınırında dar bir şekilde mock yapın.
- Sistem sınırlarında mock yapın: üçüncü taraf API'ler, SDK'lar, ödeme ağ geçitleri, yerel olarak çalıştıramadığınız harici servisler.
- Kendi soyutlamalarınız (repository'ler, önbellekler, kuyruklar) için onları mock'lamak yerine bellek içi test ikizleri kullanın.
- Size ait olmayan şeyleri mock'lamaktan kaçının — kütüphane iç yapılarını mock'lamak testlerinizi uygulama detaylarına bağlar.
- Modül seviyesinde mock'lama yerine bağımlılık enjeksiyonunu tercih edin. Bağımlılıkları açıkça geçmek test kurulumunu şeffaf hale getirir.
- Mock kapsamını ihtiyaç duyan testle sınırlayın. vi.resetAllMocks veya benzeri ile testler arasında tüm mock'ları sıfırlayın.
CI/CD hatlarında test
Bir test takımı ancak tutarlı bir şekilde çalışıyorsa ve hızlı geri bildirim veriyorsa değerlidir. Yavaş, tutarsız veya yalnızca geliştirici makinelerinde çalışan testler bir yükümlülüktür. Testleri CI/CD hattınıza entegre etmek, her çekme isteğinde, main'e her birleştirmede ve kritik yollar için her dağıtımdan önce çalışmalarını sağlar.
Test piramidi, doğal olarak CI hattı aşamalarına dönüşür. Birim testleri, hızlı oldukları ve temel mantık hatalarını yakaladıkları için her push'ta önce çalışır. Entegrasyon testleri, mümkün olduğunda paralel olarak, konteynerize bağımlılıklar kullanarak sonra çalışır. E2E testleri, yavaş ve pahalı oldukları için yalnızca çekme isteklerinde ve dağıtımlardan önce en son çalışır. Görsel regresyon testleri, E2E testleriyle birlikte çalışarak ekran görüntülerini main dalındaki temel görüntülerle karşılaştırır.
Hat performansı önemlidir. Çalışması 45 dakika süren bir test takımı, geliştiricilerin testleri yerel olarak çalıştırmayı atlamasına ve bozuk kod göndermesine teşvik eder. CI'yı hızlı tutma stratejileri arasında yalnızca mevcut değişiklikten etkilenen testleri çalıştırma (etkilenen test tespiti), test takımlarını paralel çalıştırıcılara bölme, node_modules ve Playwright tarayıcılarını çalıştırmalar arasında önbellekleme ve yavaş E2E testlerini her commit yerine bir zamanlamayla çalıştırma yer alır.
# .github/workflows/test.yml — örnek hat yapısı
name: test
on: [push, pull_request]
jobs:
unit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm ci
- run: npx vitest run --project unit
integration:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_DB: test
POSTGRES_PASSWORD: test
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx vitest run --project integration
e2e:
needs: [unit, integration]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright testTutarsız testler, CI güvenine yönelik en büyük tehdittir. Kod değişiklikleriyle ilgisi olmayan nedenlerle aralıklı olarak başarısız olan bir test, tüm hatta olan güveni aşındırır. Ekipler test başarısızlıklarını görmezden gelmeye, kırmızı CI ile kod birleştirmeye başlar ve sonunda test takımı gürültüye dönüşür. Tutarsız test tespitine yatırım yapın — tutarsız testleri işaretleyin, karantinaya alın ve düzeltmeye veya kaldırmaya öncelik verin. Güvenilmeyen bir test, hiç test olmamasından daha kötüdür çünkü başarısızlıkları görmezden gelme alışkanlığı yaratır.
Test sonucu raporlaması, bir diğer hafife alınan CI uygulamasıdır. Bağlam olmadan "3 test başarısız oldu" diyen bir test çıktısı, geliştiricilerin gerçek hata mesajını bulmak için günlüklerde kaydırmasına neden olur. İyi raporlama, başarısız olan iddiayı, beklenen ve gerçekleşen değerleri, test dosyasını ve satır numarasını ve ekran görüntüleri veya yığın izleri gibi ilgili bağlamı gösterir. vitest-html-reporter, Playwright'ın HTML raporlayıcısı ve GitHub Actions ek açıklamaları gibi araçlar, test sonuçlarını doğrudan çekme isteği iş akışında görünür kılar.
Test trophy ve piramit tartışması
Geleneksel test piramidi — tabanda birim testleri, ortada entegrasyon testleri, tepede E2E testleri — yirmi yıldır test stratejisine rehberlik etmiştir. Piramit basit bir fikri iletir: çok sayıda hızlı izole test, daha az sayıda yavaş entegrasyon testi ve çok az sayıda yavaş E2E testi yazın. Bu model, dağıtım biriminin net girdi-çıktı sınırları olan bir fonksiyon veya sınıf olduğu arka uç servisleri için iyi çalışır.
Kent C. Dodds, modern ön yüz geliştirmeyi daha iyi yansıtan alternatif bir model olarak test trophy'yi önermiştir. Trophy, piramidi, entegrasyon testlerinin en büyük katmanı oluşturduğu, daha az birim testi ve daha az E2E testi bulunan bir elmasa dönüştürür. Argüman, ön yüz uygulamaları için en büyük değerin, bileşenlerin birlikte nasıl çalıştığını test etmekten geldiğidir — ki bu tam olarak entegrasyon testlerinin doğruladığı şeydir — izole fonksiyonları veya tam kullanıcı yolculuklarını test etmekten daha değerlidir.
Trophy modeli pratik bir gerçeği yansıtır: bir React uygulamasının en değerli testleri, bir bileşeni render eden, onunla etkileşime giren ve sonuç üzerinde iddialar yapan testlerdir. Bu testler, bileşeni, alt bileşenlerini, hook'larını, durum yönetimini ve API çağrılarını birlikte çalıştırır. E2E testlerinden daha hızlı, ancak birim testlerinden daha gerçekçidir. İyi yazılmış bir entegrasyon testi takımı, bir ön yüz bağlamında birim veya E2E testlerinden test başına daha yüksek güven sağlar.
Gerçek şu ki, her iki model de daha karmaşık bir gerçeğin basitleştirmeleridir. Doğru test stratejisi, uygulama mimarinize bağlıdır. Ağır iş mantığına sahip bir arka uç mikroservisi, piramitten faydalanır — alan mantığının derin birim test kapsamı, API katmanı için daha az entegrasyon testi ve harici servis etkileşimleri için bir avuç sözleşme testi. Karmaşık kullanıcı etkileşimlerine sahip bir ön yüz uygulaması, trophy'den faydalanır — bileşenlerin ve sayfaların geniş entegrasyon testi kapsamı, yardımcı mantık için seçici birim testleri ve kritik yol E2E testleri.
Piramit ve trophy'nin ikisi de dogmatik bir şekilde takip edilirse yanlıştır. Doğru test stratejisi, hatalarınızı yakalayan, ekibiniz için yeterince hızlı çalışan ve mimarinize dayanan stratejidir. Bu bir piramide, bir trophy'e veya henüz kimsenin adlandırmadığı bir şekle benzeyebilir. Şekil hakkında tartışmayı bırakın ve testlerinizin gerçekte ne yakaladığına bakmaya başlayın.
Her iki modelin ardındaki önemli içgörü, farklı test seviyelerinin farklı maliyet-fayda profillerine sahip olduğudur. Bir birim testi yazılması ve çalıştırılması milisaniyeler sürer. Bir entegrasyon testi saniyeler sürer. Bir E2E testi dakikalar sürer. Test bütçenizi, her seviyenin belirli uygulamanız için sağladığı değerle orantılı olarak ayırmalısınız. Uygulamanız karmaşık iş mantığına sahip veri odaklı bir gösterge tablosuysa, birim ve entegrasyon testlerine yatırım yapın. Uygulamanız minimum etkileşime sahip bir içerik sitesiyse, kritik akışlar için E2E testleri ve düzen için görsel regresyon testleri en yüksek değerli yatırım olabilir.
Bir test kültürü oluşturmak
En iyi test stratejisi bile, ekip test yazmaya inanmıyorsa başarısız olur. Test kültürü, kapsama eşikleri dayatmak veya TDD'yi zorunlu kılmakla ilgili değildir. Test yazmayı doğal olarak en az dirençli yol haline getiren bir ortam yaratmakla ilgilidir. Geliştiriciler, testler yazması kolay, çalıştırması hızlı ve açıkça değerli olduğunda test yazar. Testler kırılgan, yavaş ve yanlış başarısızlıklar ürettiğinde, geliştiriciler onlardan kaçınmanın yollarını bulur.
Test deneyimini mükemmel hale getirerek başlayın. Test altyapısına yatırım yapın — hızlı CI çalıştırıcıları, güvenilir test veritabanları, tutarsız test tespiti. Yaygın iddiaları kısa ve öz hale getiren test yardımcı programları yazın. Her geliştiricinin sıfırdan başlamadan yeni bir API uç noktasını, React bileşenini veya veritabanı migrasyonunu nasıl test edeceğini bilmesi için bir ekip test rehberinde test kalıplarını belgeleyin. İyi test altyapısını bir kez kurmanın maliyeti, bir ekibin kötü testlerle her gün savaşmasının birikmiş maliyetinden çok daha düşüktür.
Kod incelemeleri test incelemesini içermelidir. İncelemeciler şunu sormalıdır: bu test, iddia ettiği davranışı kapsıyor mu? Uygulama yanlış olsa bile geçebilir mi? Doğru şeyi doğru seviyede test ediyor mu? Okunabilir ve bakımı yapılabilir mi? Testlere birinci sınıf kod muamelesi yapın — üretim koduyla aynı inceleme standartlarına, stil kılavuzlarına ve kalite beklentilerine tabi tutun.
Kapsama metrikleri yararlı bir teşhis aracıdır ancak korkunç bir hedeftir. Yüzde 90 kapsama hedefi belirlerseniz, yüzde 90 kapsama alırsınız — daha iyi testler değil. Geliştiriciler, güvenlik ağını iyileştirmeden hedefi karşılamak için getter metotları ve boş yapıcılar üzerinde iddialar yazan önemsiz testler yazacaktır. Kapsama raporlarını test edilmemiş kod yollarını bulmak için kullanın, keyfi eşikler dayatmak için değil. İyi yerleştirilmiş entegrasyon testleriyle yüzde 70 kapsama, sığ birim testleriyle yüzde 95 kapsamadan daha değerlidir.
- Her test dosyasındaki tekrarı azaltan test yardımcı programları, fabrikalar ve yardımcılar sağlayarak test yazmayı kolaylaştırın.
- Test iyileştirmelerini, özellik teslimatını kutladığınız gibi kutlayın. Bir hata sınıfını ortadan kaldıran bir test bir özelliktir.
- Test altyapısı sahipliğini dönüşümlü olarak devredin, böylece bilgi tüm ekibe yayılır, tek bir kişide yoğunlaşmaz.
- Düzenli tutarsız test triyaj oturumu yapın. Tutarsız test sayılarını bir ekip sağlığı metriği olarak izleyin ve düzeltmelere öncelik verin.
- Her seviyede neyin test edileceğini, neyin test edilmeyeceğini ve üzerinde anlaşılan ödünleşimleri tanımlayan bir ekip test manifestosu yazın.
Bir test kültürünün nihai hedefi belirli bir kapsama numarası veya mükemmel bir CI hattı değildir. Güvendir. Bir yeniden düzenlemenin üretimi bozmayacağına dair güven. Cuma öğleden sonra dağıtım yapmanın güvenli olduğuna dair güven. Bir hata raporlandığında, düzeltmenin kalıcı olacağına dair güven. Ekibe bu güveni veren bir test stratejisi, piramit, trophy veya ikisinin arası bir şeye benzese de benzemese de iyi bir stratejidir.
Hepsini bir araya getirmek
Etkili bir test stratejisi oluşturmak, bir yaklaşımı diğerine tercih etmekle ilgili değildir. Ödünleşimleri anlamak ve her durum için doğru aracı uygulamakla ilgilidir. Birim testleri mantık hatalarını milisaniyeler içinde yakalar. Entegrasyon testleri, gerçek bağımlılıklarla etkileşim hatalarını yakalar. E2E testleri, tüm yığın boyunca kullanıcıya yönelik başarısızlıkları yakalar. Özellik tabanlı testler, varlığından haberdar olmadığınız uç durumları yakalar. Görsel regresyon testleri, istenmeyen görsel değişiklikleri yakalar.
Güvenilir bir şekilde teslimat yapan ekipler, en yüksek kapsama numaralarına sahip ekipler değildir. Her test seviyesinin ne sağladığı hakkında eleştirel bir şekilde düşünen, test yatırımlarını risk profilleriyle uyumlu hale getiren ve testlerin bir politika gerektirdiği için değil, değer kattığı için yazıldığı bir kültür inşa eden ekiplerdir. Mevcut test takımınızı denetleyerek başlayın. Her test için şunu sorun: bu test var olmayı hak ediyor mu? Cevap net bir evet değilse, testi kaldırın veya değiştirin. En iyi test takımı en büyük olanı değildir. Çalışma süresi dakikası başına size en çok güveni verendir.
