Küçük projelerde test stratejisi: neyi test etmeli, neyi hiç etmemeli
Tek kişilik ya da küçük bir projede test yazmak lüks gibi görünür. Aslında sorun testin kendisi değil, yanlış yere yazılan test. Bir buton sınıfının adını kontrol eden elli test, yeniden düzenlemede kırılır ve kimseyi korumaz. Fiyat hesabını kontrol eden beş test ise bir gün sizi gerçekten kurtarır.
Bu yazı, portföyümdeki projelerde test yazarken hangi kararların verildiğini ve nedenlerini anlatıyor. Hepsinde aynı iki araç var: saf mantık için Vitest, gerçek tarayıcıda kullanıcı akışı için Playwright.
İki katman çoğu zaman yeter
Birim testi bir fonksiyonu tek başına çalıştırır: girdi verin, çıktıyı kontrol edin. Milisaniyeler sürer, hata olduğunda tam yeri gösterir. Uçtan uca test ise uygulamayı gerçek tarayıcıda açıp bir kullanıcı gibi tıklar. Yavaştır, ama "sistem bir bütün olarak çalışıyor mu" sorusuna cevap veren tek test odur.
Küçük projede arada kalan katmanlara (bileşen testleri, sözleşme testleri, entegrasyon testleri) çoğu zaman gerek kalmıyor. Kendi projelerimdeki dağılım şöyle:
| Proje | Birim testi | Uçtan uca |
|---|---|---|
| Klinik yönetim paneli (Next.js) | 11 Vitest testi | 3 rol için 30 kontrol |
| Inkway (tarayıcı oyunu) | 22 Vitest testi | yok, Playwright yalnızca ekran görüntüsü alıyor |
| Shadow Lock (tarayıcı oyunu) | 14 Vitest testi | yok |
| Hopscraper (tarayıcı oyunu) | yok | 6 test, gerçek Chromium'da |
| Kahve e-ticaret sitesi | yok | 18 kontrol |
Tablodaki boşluklar bilinçli seçimler, bir kısmı da dürüstçe söylemek gerekirse eksik. Aşağıda ikisi de açılıyor.
Birim testiyle neyi koruyorum
Kuralım basit: hata yaptığında para, randevu ya da güven kaybettiren saf fonksiyonlar birim testi alır. Klinik yönetim panelinde bunlar randevu çakışması, mesai kontrolü, doluluk oranı, hatırlatma planı, rapor serileri, CSV çıktısı, oturum imzası ve rol yetkileri. Çakışma testinin bir parçası şöyle:
it("çakışan aralıkları yakalar, bitişikleri serbest bırakır", () => {
expect(findConflict(ex, "10:15", 30)?.id).toBe(1);
expect(findConflict(ex, "09:30", 30)).toBeNull();
expect(findConflict(ex, "10:30", 30)).toBeNull();
});
it("iptal edilen randevular engel değildir; kendi kaydı yok sayılır", () => {
expect(findConflict(ex, "11:00", 60)).toBeNull();
expect(findConflict(ex, "10:00", 30, 1)).toBeNull();
});
Test adlarına bakın: her biri bir iş kuralını cümle olarak söylüyor. "09:30'da başlayan 30 dakikalık randevu, 10:00'daki randevuyla çakışmaz" gibi sınır durumları, elle test ederken en kolay atladığınız durumlardır.
Bir de tasarım dersi var. Hatırlatma planını hesaplayan planReminders fonksiyonu "şu an"ı kendisi okumuyor, parametre olarak alıyor. Test de sabit bir tarih veriyor. Fonksiyonun içinde new Date() olsaydı, test yazıldığı gün geçer, bir hafta sonra kırılırdı. Zamanı ve rastgeleliği dışarıdan vermek, test yazmayı kolaylaştıran en ucuz değişiklik.
Kahve e-ticaret sitesinde ise birim testi yok. Fiyatlar istemcide değil sunucuda hesaplanıyor: gramaj fiyatı ve kargo sabitleri lib/pricing.ts içinde, kupon indirimi, kargo ücreti ve sipariş toplamı ise sipariş eyleminin (Server Action) içinde. Bu mantık yalnızca uçtan uca akışla dolaylı olarak test ediliyor. Bugün o projeye tek bir şey ekleyecek olsam, kupon ve toplam hesabını saf bir fonksiyona çıkarıp ona birim testi yazardım.
Oyun mantığını tarayıcısız test etmek
Oyunlar ilk bakışta test edilemez görünür: her şey tuvale çiziliyor, zamanla ilerliyor, rastgele. Inkway ve Shadow Lock'ta bu üç kararla çözüldü.
Birincisi, oyun mantığı çizimden ayrı. Oyun sınıfının bir step(dt) metodu var ve Canvas'a hiç dokunmuyor. Çizim ayrı bir draw metodunda. Bu sayede Vitest testleri environment: "node" ile, tarayıcı ya da sahte DOM olmadan çalışıyor.
İkincisi, rastgelelik değiştirilebilir. Rastgele sayı fonksiyonları tek bir modülden geliyor ve testler her seferinde tohumlu bir üreteç takıyor: setRandom(seeded(7)). Aynı test her çalıştığında aynı bölümü, aynı olayları görüyor.
Üçüncüsü, zamanı test sürüyor. Küçük bir yardımcı oyunu saniyelerce, sabit 1/60 adımlarla ilerletiyor ve bir koşul sağlanınca duruyor:
const DT = 1 / 60;
function run(p: Play, seconds: number, until?: () => boolean) {
for (let i = 0; i < seconds * 60; i++) {
p.step(DT);
input.pressed = false;
input.released = false;
if (until?.()) return;
}
}
it("loses a heart when the blob drops into the first gap", () => {
const p = new Play();
run(p, 10, () => p.state === "dead");
expect(p.state).toBe("dead");
expect(p.hp).toBe(2);
});
Oyuncunun parmağı da aynı şekilde taklit ediliyor: input.down = true; input.pressed = true; deyip bir adım atınca oyun bir çizginin başladığını sanıyor. Böylece "yağmurdaki çizgi dört kat hızlı kuruyor mu" ya da "şişeye ulaşınca kalan mürekkebin onda biri kadar bonus geliyor mu" gibi kurallar tarayıcı açmadan, saniyenin altında test ediliyor. Shadow Lock'ta bir test, şekil doğru açıya geldiğinde dokunan bir oyuncu gibi dört bölümü baştan sona bitiriyor ve skorun kurallara göre çıktığını doğruluyor.
Hopscraper farklı bir örnek. Oyun tek bir betik dosyası, modül olarak içe aktarılamıyor. Bölmek yerine testleri gerçek Chromium'da çalıştırdım: Playwright sayfayı açıyor, oyun geliştirme için bazı fonksiyonları window.__hop altında açığa çıkarıyor, test de page.evaluate ile simülasyonu doğrudan çağırıyor. Daha yavaş, ama kodu yeniden düzenlemeden en değerli testi yazmamı sağladı: aynı tohum ve aynı girdiler her zaman aynı skoru veriyor mu.
Uçtan uca test: az, ama gerçek akış
Uçtan uca testte her şey denenmiyor. Yalnızca bozulduğunda işin durduğu akışlar seçiliyor. Klinik yönetim panelinde bunlar oturumsuz erişimin giriş sayfasına yönlenmesi, hatalı parolanın reddedilmesi, her rolün yalnızca kendi menüsünü görmesi, hasta formunun doğrulaması ve yeni hasta kaydı. Kahve e-ticaret sitesinde sepetten ödemeye kadar olan yol.
Bu testleri yazarken işe yarayan birkaç alışkanlık:
- Öğeler
getByLabel("E-posta")vegetByRole("button", { name: "Kaydet" })ile bulunuyor, CSS seçiciyle değil. Tasarım değişince test kırılmıyor, ayrıca etiketi olmayan bir alan varsa test bunu yakalıyor. - Her sayfada
pageerrorve konsol hataları toplanıyor. Kullanıcıya görünmeyen ama konsola düşen hatalar da böylece ortaya çıkıyor. - Testleri geliştirme sunucusunda değil,
npm run build && npm startile üretim derlemesinde çalıştırıyorum. Bazı hatalar yalnızca derlemede çıkıyor. - Test verisinin demo veriyle çakışmaması için randevu testleri üç hafta sonrasındaki boş bir pazartesiyi kullanıyor.
Bu projelerde Playwright'ı kendi test koşucusu (@playwright/test) yerine kütüphane olarak, düz bir Node betiğiyle kullandım. Sebebi, aynı betiğin README'deki ekran görüntülerini de alması. Daha büyük bir projede paralel çalıştırma ve raporlama için koşucuyu tercih ederim.
Neyi test etmiyorum
Test yazmamanın da gerekçesi var. Şunlara zaman harcamıyorum:
- Çatının kendi davranışı. Next.js'in yönlendirmesinin ya da Zustand'ın veriyi saklamasının çalıştığını test etmek, başkasının test ettiği şeyi tekrar etmek demek.
- Uygulama ayrıntıları. Bir bileşenin iç durumunun şekli, CSS sınıf adları, bir fonksiyonun kaç kez çağrıldığı. Bunlar yeniden düzenlemede kırılır ama hiçbir hatayı yakalamaz.
- Büyük HTML anlık görüntüleri (snapshot). Değiştiğinde kimse farkı okumaz, "güncelle" deyip geçer.
- Tuvalin piksel çıktısı. Oyunlarda çizimi değil, durumu test ediyorum: topun konumu, can sayısı, skor.
- Yüzde yüz kapsama hedefi. Kapsama oranı hangi satırın çalıştığını söyler, doğru çalışıp çalışmadığını söylemez.
Nereden başlamalı
Hiç testi olmayan bir projeye test ekleyecekseniz şu sırayı öneririm:
- Para, tarih ya da yetkiyle ilgili hesapları saf fonksiyonlara ayırın. Zamanı ve rastgeleliği parametre yapın.
- Bu fonksiyonlara sınır durumlarını da kapsayan birkaç birim testi yazın.
- Projenin varlık sebebi olan tek akışı (ödeme, randevu, kayıt) uçtan uca test edin ve konsol hatalarını toplayın.
- Bir hata bulduğunuzda önce onu yakalayan testi yazın, sonra düzeltin.
Bu dört adım bir günde biter ve çoğu küçük proje için yeterlidir. Gerisini, bir şey gerçekten kırıldığında eklersiniz.