Blok 8 · Ders 53
Test yazmak ve kod kalitesi
node:test ile birim testleri, test edilebilir fonksiyon yazmak, ESLint ve Prettier ile düzenli kod, GitHub Actions ile otomatik test.
Bu dersin sonunda
node:testvenode:assert/strictile test dosyası yazıpnode --testçıktısında geçen ve kalan testleri okuyabilirsin- Bir fonksiyonun sınır durumlarını (sıfır, eşitlik, boş liste, hatalı girdi) bulup her biri için test yazabilirsin
- Önce testi yazıp kırmızı, yeşil, düzenle adımlarıyla (TDD) küçük bir fonksiyonu geliştirebilirsin
- Vitest ve Testing Library ile bir React bileşenini kullanıcının gözünden test edebilirsin
- ESLint ve Prettier'ı kurup uyarılarını okuyabilir, kodu tek komutla biçimlendirebilirsin
- GitHub Actions iş akışı yazıp her push'ta testlerin kendiliğinden çalışmasını sağlayabilirsin
Bu sayfada
- Neden test: güvenle değiştirmek
- Dersin projesi: oyun-hesap
- assert: beklediğini yaz, Node denetlesin
- node:test: testlere ad vermek
- Birim testi ve sınır durumları
- Önce test (TDD) denemesi
- Asenkron fonksiyonları test etmek
- React bileşen testine ilk bakış
- ESLint ve Prettier: kod kalitesinin iki bekçisi
- GitHub Actions: her push'ta test
- Görevler
- Kendini sına
- Kayıt noktası
Şimdiye kadar yazdığın her programı çalıştırıp çıktısına gözle baktın. Bu derste o bakışı koda çeviriyorsun: "bu fonksiyon şu girdiyle şunu döndürmeli" cümlesini bir kez yazacak, sonra her değişiklikte bilgisayara tek komutla denetleteceksin. Ardından kodunu temiz tutan iki aracı (ESLint ve Prettier) kuracak ve GitHub'a her gönderdiğinde bu denetimlerin kendiliğinden çalışmasını sağlayacaksın. İş ilanlarında "test yazabilen" diye geçen beceri tam olarak bu.
Neden test: güvenle değiştirmek
Ders 41'in sonunda "Sınırlarda dene" diye bir liste vardı: seviye 1'de eksiye bas, takım boşken bak, toplam tam altına eşitken bak… Altı adımı elle denedin. Peki yarın TakimPaneli'ne küçük bir özellik eklediğinde o altı adımı yeniden yapacak mısın? Bir ay sonra? Arkadaşın senin kodunu değiştirdiğinde o da yapacak mı?
Dürüst cevap: yapmazsın. Elle deneme unutulur, sıkıcıdır, her seferinde biraz eksik yapılır. Sonra bir gün "kısaltayım" dediğin bir satır, üç hafta önce çalışan bir şeyi sessizce bozar ve sen bunu oyuncudan duyarsın.
Çözüm, denemeleri bir kez kod olarak yazmak. Her deneme bir girdi verir, fonksiyonun döndürdüğünü beklediğin değerle karşılaştırır ve "tuttu" ya da "tutmadı" der. Bütün denemeleri tek komutla, bir saniyeden kısa sürede çalıştırırsın.
Kırmızı bir test kötü haber değildir: hatayı oyuncudan önce sen gördün demektir. Asıl kötü haber, hiç testin olmadığı için hiçbir şeyin kırmızı yanmamasıdır.
Dersin projesi: oyun-hesap
Bu derste önceki derslerden tanıdığın küçük oyun hesaplarını bir araya toplayıp test edeceksin. Yeni bir klasör aç. VS Code'un terminalinde sırayla:
cd C:\kodmkdir oyun-hesapcd oyun-hesapgit initnpm init -ynpm pkg set type=modulecode .npm pkg set type=module komutu package.json'a "type": "module" satırını yazar; böylece import / export kullanabilirsin (Ders 14). Açılan pencerede iki dosya oluştur. İlki Ders 9'daki KDA fonksiyonu, bu kez dışa aktarılmış hâliyle:
export function kdaMetni(kill, olum, asist) { if (olum === 0) { return "Kusursuz" } return ((kill + asist) / olum).toFixed(2)}İkincisi Ders 1'in son görevindeki altın paylaşımı. Orada ekrana yazdırıyordun; şimdi sonucu bir obje olarak döndüren bir fonksiyon:
export function altinPaylas(toplam, kisiSayisi) { const artan = toplam % kisiSayisi const kisiBasi = (toplam - artan) / kisiSayisi return { kisiBasi, artan }}İki dosya da yalnızca fonksiyon dışa aktarıyor, ekrana bir şey yazmıyor. Böyle bir fonksiyonu test etmek en kolayıdır: girdi verirsin, dönen değere bakarsın. Ders 10'daki saf fonksiyon fikri burada işe yarıyor.
assert: beklediğini yaz, Node denetlesin
Node'un içinde, beklentini denetleyen hazır bir modül var: node:assert. Kurulum gerektirmez. Klasörde deneme.js aç:
import assert from "node:assert/strict"import { kdaMetni } from "./kda.js" assert.strictEqual(kdaMetni(7, 2, 9), "8.00")assert.strictEqual(kdaMetni(10, 0, 6), "Kusursuz")console.log("İki denetim de geçti")PS C:\kod\oyun-hesap> node deneme.jsİki denetim de geçtiassert.strictEqual(gercek, beklenen) şunu der: "soldaki değer, sağdakiyle === anlamında aynı olmalı." Aynıysa hiçbir şey olmaz, program yoluna devam eder. Bu yüzden son satıra kadar geldik.
Şimdi 4. satırdaki beklentiyi bilerek yanlış yaz: "8.00" yerine tırnaksız 8.
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:Ne zaman çıkar? assert.strictEqual'a verdiğin iki değer === ile eşit değilken. Değerler aynı görünse bile türleri farklıysa (metin "8.00" ile sayı 8) çıkar.
import assert from "node:assert/strict"import { kdaMetni } from "./kda.js" assert.strictEqual(kdaMetni(7, 2, 9), 8)assert.strictEqual(kdaMetni(10, 0, 6), "Kusursuz")console.log("İki denetim de geçti")node:internal/modules/run_main:107 triggerUncaughtException( ^ AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: '8.00' !== 8 at file:///C:/kod/oyun-hesap/deneme.js:4:8 ... at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5) { generatedMessage: true, code: 'ERR_ASSERTION', actual: '8.00', expected: 8, operator: 'strictEqual', diff: 'simple'} Node.js v24.20.0Nasıl okunur? İlk üç satır Node'un kendi iç dosyasını gösteriyor (import kullanan dosyalarda hata böyle başlar), geç. Asıl bilgi ortada: '8.00' !== 8, yani "gelen '8.00', beklenen 8, ikisi aynı değil". Tek tırnak, gelenin metin olduğunu söylüyor; toFixed metin döndürür (Ders 9). Satır numarası at file:///…/deneme.js:4:8 satırında: 4. satır, 8. sütun.
Nasıl düzelir? Ya beklentin yanlıştır (burada öyle: fonksiyon metin döndürmeli, beklenen de metin olmalı) ya da fonksiyonun. Hangisinin yanlış olduğuna sen karar verirsin; test yalnızca ikisinin uyuşmadığını söyler.
Program 4. satırda durdu, 5. satır hiç çalışmadı: assert bir hata fırlatır ve yakalanmayan hata programı durdurur (Ders 13). Beklentiyi yeniden "8.00" yap; sonra deneme.js'i sil, işi bitti.
Objeleri karşılaştırmak: deepEqual
altinPaylas bir obje döndürüyor. strictEqual ile denetlemeye kalkarsan sürpriz var:
import assert from "node:assert/strict" const beklenen = { kisiBasi: 246, artan: 4 }const gelen = { kisiBasi: 246, artan: 4 } console.log(gelen === beklenen)assert.deepEqual(gelen, beklenen)console.log("deepEqual: içerikleri aynı")assert.strictEqual(gelen, beklenen)console.log("bu satıra gelinmez")falsedeepEqual: içerikleri aynınode:internal/modules/run_main:107 triggerUncaughtException( ^ AssertionError [ERR_ASSERTION]: Values have same structure but are not reference-equal: { artan: 4, kisiBasi: 246} at file:///C:/kod/oyun-hesap/nesne.js:9:8 ... Node.js v24.20.0İki obje aynı alanları aynı değerlerle taşıyor ama === yine false diyor, çünkü ikisi bellekte iki ayrı obje; === objelerde "aynı obje mi" diye referansa bakar (Ders 12). Mesaj da bunu söylüyor: same structure but not reference-equal, "yapıları aynı ama aynı obje değiller".
assert.deepEqual ise objenin içine bakar: her alanı tek tek, iç içe objelere ve dizilere kadar karşılaştırır. Kural basit:
- Sayı, metin,
true/false,null,undefinedkarşılaştırıyorsan:assert.strictEqual. - Obje ya da dizi karşılaştırıyorsan:
assert.deepEqual. - "Bu çağrı hata fırlatmalı" diyorsan:
assert.throws(birazdan). - "Bu Promise reddedilmeli" diyorsan:
assert.rejects(birazdan).
nesne.js yalnızca bir denemeydi, onu da sil.
node:test: testlere ad vermek
deneme.js'in iki sorunu var: ilk tutmayan satırda her şey duruyor ve hangi denetimin ne için yazıldığı belli değil. Node'un ikinci hazır modülü node:test bunları çözer. Denetimleri adlandırılmış testlere böler, hepsini çalıştırır ve sonunda bir rapor verir.
Klasörde kda.test.js aç. Adın sonundaki .test.js önemli; birazdan nedenini göreceksin.
import { test } from "node:test"import assert from "node:assert/strict"import { kdaMetni } from "./kda.js" test("ölüm varsa KDA iki basamakla yazılır", () => { assert.strictEqual(kdaMetni(7, 2, 9), "8.00")}) test("hiç ölmeyen oyuncu Kusursuz sayılır", () => { assert.strictEqual(kdaMetni(10, 0, 6), "Kusursuz")}) test("küsurat iki basamağa yuvarlanır", () => { assert.strictEqual(kdaMetni(1, 8, 2), "0.38")})PS C:\kod\oyun-hesap> node --test✔ ölüm varsa KDA iki basamakla yazılır (0.6462ms)✔ hiç ölmeyen oyuncu Kusursuz sayılır (0.1029ms)✔ küsurat iki basamağa yuvarlanır (0.0985ms)ℹ tests 3ℹ suites 0ℹ pass 3ℹ fail 0ℹ cancelled 0ℹ skipped 0ℹ todo 0ℹ duration_ms 71.3322Her test(...) raporda kendi adıyla bir satır olur. node --test klasördeki test dosyalarını kendisi bulur.
test iki şey alır: bir ad (raporda görünen cümle) ve bir fonksiyon (çalıştırılacak denetimler). Fonksiyonun içinde istediğin kadar assert olabilir; biri tutmazsa o test kalır, ötekiler yine çalışır.
Raporun alt kısmındaki ℹ satırları özet: tests 3 (kaç test çalıştı), pass 3 (kaçı geçti), fail 0 (kaçı kaldı). suites, skipped, todo ve cancelled satırlarını şimdilik geç. Parantez içindeki süreler (0.6462ms) her çalıştırmada biraz farklı çıkar; sende de farklı olacak.
npm test: projenin standart test komutu
Isınmadaki soruyu hatırla: package.json'daki test betiği şu an bir yer tutucu.
PS C:\kod\oyun-hesap> npm test > oyun-hesap@1.0.0 test> echo "Error: no test specified" && exit 1 "Error: no test specified"Onu gerçek komutla değiştir. npm pkg set dosyayı senin yerine düzenler:
PS C:\kod\oyun-hesap> npm pkg set scripts.test="node --test"PS C:\kod\oyun-hesap> npm test > oyun-hesap@1.0.0 test> node --test ✔ ölüm varsa KDA iki basamakla yazılır (0.6524ms)✔ hiç ölmeyen oyuncu Kusursuz sayılır (0.101ms)✔ küsurat iki basamağa yuvarlanır (0.0964ms)ℹ tests 3ℹ suites 0ℹ pass 3ℹ fail 0ℹ cancelled 0ℹ skipped 0ℹ todo 0ℹ duration_ms 72.1739npm test, npm start gibi özel bir kısayoldur: başına run yazmana gerek yok (npm betiği). Bir projeyi yeni açan herkes testleri bu komutla çalıştırmayı dener; birazdan GitHub da öyle yapacak.
Kırmızıyı okumak
Testin asıl değeri bir şey bozulduğunda ortaya çıkar. Diyelim ki bir gün kda.js'i toFixed yerine Math.round ile, sana "daha temiz" gelen bir yuvarlamayla yeniden yazdın:
export function kdaMetni(kill, olum, asist) { if (olum === 0) { return "Kusursuz" } const kda = (kill + asist) / olum return String(Math.round(kda * 100) / 100)}0.38 gibi sayılarda aynı sonucu verdiği için elle denesen fark etmeyebilirdin. Testler fark etti:
PS C:\kod\oyun-hesap> node --test✖ ölüm varsa KDA iki basamakla yazılır (1.1377ms)✔ hiç ölmeyen oyuncu Kusursuz sayılır (0.1425ms)✔ küsurat iki basamağa yuvarlanır (0.1096ms)ℹ tests 3ℹ suites 0ℹ pass 2ℹ fail 1ℹ cancelled 0ℹ skipped 0ℹ todo 0ℹ duration_ms 71.4504 ✖ failing tests: test at kda.test.js:5:1✖ ölüm varsa KDA iki basamakla yazılır (1.1377ms) AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: '8' !== '8.00' at TestContext.<anonymous> (file:///C:/kod/oyun-hesap/kda.test.js:6:12) ... at startSubtestAfterBootstrap (node:internal/test_runner/harness:387:17) { generatedMessage: true, code: 'ERR_ASSERTION', actual: '8', expected: '8.00', operator: 'strictEqual', diff: 'simple' }Math.round(800) / 100 sonucu 8 olur ve String(8) "8" verir; sondaki sıfırlar kayboldu. Maç geçmişinde bir satırda 8.00, ötekinde 8 yazması küçük ama gerçek bir gerileme. Rapor sana nereye bakacağını söylüyor:
... ile kısalttığımız yerde Node'un kendi dosyalarından geçen satırlar var.Şimdi karar senin: yeni davranış mı doğru, eski mi? Burada eskisi doğru, çünkü tablo iki basamak bekliyor. kda.js'i toFixed'li hâline geri al ve npm test ile yeniden yeşili gör. Bazen de yeni davranış doğrudur; o zaman testi güncellersin. Test kararı senin yerine vermez, ama kararı bilerek vermeni sağlar.
ℹ tests 0: test dosyası bulunamadıNe zaman çıkar? Test dosyasının adı node --test'in aradığı kalıba uymayınca. Örneğin kda.test.js yerine kdaTest.js yazınca:
PS C:\kod\oyun-hesap> node --testℹ tests 0ℹ suites 0ℹ pass 0ℹ fail 0ℹ cancelled 0ℹ skipped 0ℹ todo 0ℹ duration_ms 10.5027Tehlikeli olan şu: fail 0 yazıyor ve komut başarıyla bitiyor. Hiçbir şey denetlenmedi ama her şey yeşil görünüyor.
Nasıl düzelir? Dosya adını .test.js ile bitir. node --test adı .test.js, -test.js ya da _test.js ile biten, test- ile başlayan ya da test adlı bir klasörün içindeki dosyaları bulur. Bu kursta hep ad.test.js kullanıyoruz. Raporda tests sayısının beklediğin kadar olduğuna arada bir göz at.
Birim testi ve sınır durumları
Şimdiye kadar yazdığın testlerin her biri tek bir fonksiyonu, dış dünyadan (ağdan, veritabanından, ekrandan) ayrı denetliyor. Bunlara birim testi denir. Bir uygulamadaki testlerin çoğu bu türdendir: hızlıdır, ne zaman kaldığı bellidir ve hata çıktığında suçlu tek bir fonksiyondur.
altinPaylas için altin.test.js aç. Bu kez testleri describe ile bir başlık altında toplayacağız; it de test'in başka bir adıdır, "o şunu yapar" diye okunsun diye kullanılır:
import { describe, it } from "node:test"import assert from "node:assert/strict"import { altinPaylas } from "./altin.js" describe("altinPaylas", () => { it("artanı kimseye vermez", () => { assert.deepEqual(altinPaylas(1234, 5), { kisiBasi: 246, artan: 4 }) }) it("tam bölünürse artan 0 olur", () => { assert.deepEqual(altinPaylas(1235, 5), { kisiBasi: 247, artan: 0 }) }) it("altın kişi sayısından azsa herkese 0 düşer", () => { assert.deepEqual(altinPaylas(3, 5), { kisiBasi: 0, artan: 3 }) }) it("hiç altın yoksa ikisi de 0 olur", () => { assert.deepEqual(altinPaylas(0, 5), { kisiBasi: 0, artan: 0 }) })})PS C:\kod\oyun-hesap> npm test > oyun-hesap@1.0.0 test> node --test ▶ altinPaylas ✔ artanı kimseye vermez (0.9788ms) ✔ tam bölünürse artan 0 olur (0.1165ms) ✔ altın kişi sayısından azsa herkese 0 düşer (0.1072ms) ✔ hiç altın yoksa ikisi de 0 olur (0.114ms)✔ altinPaylas (2.7047ms)✔ ölüm varsa KDA iki basamakla yazılır (0.6442ms)✔ hiç ölmeyen oyuncu Kusursuz sayılır (0.1432ms)✔ küsurat iki basamağa yuvarlanır (0.1014ms)ℹ tests 7ℹ suites 1ℹ pass 7ℹ fail 0ℹ cancelled 0ℹ skipped 0ℹ todo 0ℹ duration_ms 80.2627describe bir takım (suite) açar: rapordaki ▶ altinPaylas başlığı ve altındaki girintili satırlar. Özetteki suites sayısı da artık 1. İki dosyadaki testlerin hepsi tek komutla çalıştı.
Dört testin yalnızca ilki "sıradan" bir durum. Ötekiler bilerek seçildi: tam bölünme, altının oyuncudan az olması, hiç altın olmaması. Bunlar Ders 2'den tanıdığın sınır değerleri; hatalar en çok orada saklanır. Görmek için altin.js'i tek bir return'e "kısaltmayı" dene:
export function altinPaylas(toplam, kisiSayisi) { return { kisiBasi: Math.round(toplam / kisiSayisi), artan: toplam % kisiSayisi }}PS C:\kod\oyun-hesap> node --test altin.test.js▶ altinPaylas ✖ artanı kimseye vermez (2.1932ms) ✔ tam bölünürse artan 0 olur (0.148ms) ✖ altın kişi sayısından azsa herkese 0 düşer (0.2415ms) ✔ hiç altın yoksa ikisi de 0 olur (0.1172ms)✖ altinPaylas (3.6294ms)ℹ tests 4ℹ suites 1ℹ pass 2ℹ fail 2ℹ cancelled 0ℹ skipped 0ℹ todo 0ℹ duration_ms 72.1013 ✖ failing tests: test at altin.test.js:6:5✖ artanı kimseye vermez (2.1932ms) AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal: + actual - expected { artan: 4, + kisiBasi: 247 - kisiBasi: 246 } at TestContext.<anonymous> (file:///C:/kod/oyun-hesap/altin.test.js:7:16) ... test at altin.test.js:14:5✖ altın kişi sayısından azsa herkese 0 düşer (0.2415ms) AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal: + actual - expected { artan: 3, + kisiBasi: 1 - kisiBasi: 0 } at TestContext.<anonymous> (file:///C:/kod/oyun-hesap/altin.test.js:15:16) ...node --test altin.test.js yalnız o dosyayı çalıştırır; uzun bir rapordan kaçmak için işe yarar. deepEqual'ın raporu bir fark listesidir: + ile başlayan satır gelen (actual), - ile başlayan satır beklenen (expected), işaretsiz satırlar ikisinde de aynı. Math.round, 246,8'i 247'ye yuvarlayıp olmayan altını dağıttı; 3 altını 5 kişiye bölünce de herkese 1 verdi. Tam bölünen ve sıfır durumları ise geçti, yani her deneme bu hatayı yakalamıyor. Sıradan denemenin yakalaması da biraz şanstı: 1234 yerine 1232 seçseydin 246,4 aşağı yuvarlanır, sonuç yine 246 olur ve test geçerdi. Tek bir deneme yerine farklı türden birkaç durum yazmanın sebebi bu. altin.js'i geri al.
Sınır durumlarını bulmanın yolu
Her parametre için kendine şu soruları sor:
| Soru | altinPaylas için |
|---|---|
| Sıfır olursa? | toplam 0: herkese 0. kisiSayisi 0: ? |
| Tam sınırda? | 1235 / 5: artan tam 0 |
| Sınırın hemen altında / üstünde? | 3 altın, 5 kişi: herkese 0, artan 3 |
| Boş liste, boş metin? | Burada yok; dizi alan fonksiyonlarda ilk soru budur |
| Saçma girdi? | kisiSayisi 0 ya da eksi |
Tablodaki soru işaretine bak: altinPaylas(100, 0) ne yapmalı? Şu an ne yaptığını görmek için kısa bir deneme dosyası aç, dene.js:
import { altinPaylas } from "./altin.js" console.log(altinPaylas(100, 0))PS C:\kod\oyun-hesap> node dene.js{ kisiBasi: NaN, artan: NaN }Sessizce NaN döndürüyor (Ders 9). Bu değer bir sonraki hesaba karışır ve hata çok uzakta ortaya çıkar. Daha iyisi, saçma girdiyi kapıda reddetmek (Ders 13'teki girdi doğrulama). dene.js'i sil ve bu kararı önce bir teste yaz; altin.test.js'te describe'ın içine, son it'ten sonra ekle:
it("oyuncu yoksa hata fırlatır", () => { assert.throws(() => altinPaylas(100, 0), { message: "Oyuncu sayısı en az 1 olmalı" }) })assert.throws'a fonksiyonu çağırarak değil, bir ok fonksiyonunun içinde verdin: () => altinPaylas(100, 0). Çağırsaydın hata assert.throws daha başlamadan fırlardı. Bu bir geri çağırma: "bunu sen çalıştır ve hata fırlatıp fırlatmadığına bak." İkinci parametre, beklenen hatanın message alanı. Çalıştır:
PS C:\kod\oyun-hesap> node --test altin.test.js▶ altinPaylas ✔ artanı kimseye vermez (2.7773ms) ✔ tam bölünürse artan 0 olur (0.1824ms) ✔ altın kişi sayısından azsa herkese 0 düşer (0.2237ms) ✔ hiç altın yoksa ikisi de 0 olur (0.185ms) ✖ oyuncu yoksa hata fırlatır (0.8791ms)✖ altinPaylas (6.1545ms)ℹ tests 5ℹ suites 1ℹ pass 4ℹ fail 1ℹ cancelled 0ℹ skipped 0ℹ todo 0ℹ duration_ms 138.491 ✖ failing tests: test at altin.test.js:22:5✖ oyuncu yoksa hata fırlatır (0.8791ms) AssertionError [ERR_ASSERTION]: Missing expected exception. at TestContext.<anonymous> (file:///C:/kod/oyun-hesap/altin.test.js:23:16) ...Missing expected exception: "beklenen hata gelmedi". Doğru, çünkü fonksiyon henüz hata fırlatmıyor. Şimdi altin.js'e kapıyı ekle:
export function altinPaylas(toplam, kisiSayisi) { if (kisiSayisi < 1) { throw new Error("Oyuncu sayısı en az 1 olmalı") } const artan = toplam % kisiSayisi const kisiBasi = (toplam - artan) / kisiSayisi return { kisiBasi, artan }}PS C:\kod\oyun-hesap> node --test altin.test.js▶ altinPaylas ✔ artanı kimseye vermez (3.019ms) ✔ tam bölünürse artan 0 olur (0.2167ms) ✔ altın kişi sayısından azsa herkese 0 düşer (0.181ms) ✔ hiç altın yoksa ikisi de 0 olur (0.1824ms) ✔ oyuncu yoksa hata fırlatır (0.5916ms)✔ altinPaylas (5.8644ms)ℹ tests 5ℹ suites 1ℹ pass 5ℹ fail 0ℹ cancelled 0ℹ skipped 0ℹ todo 0ℹ duration_ms 119.0806Aynı yolla ısınmadaki 0/0/0 sorununu kapat. Hiç katkısı olmayan oyuncu "Kusursuz" değil 0.00 almalı. kda.test.js'in sonuna:
test("hiç katkısı olmayan oyuncu 0.00 alır", () => { assert.strictEqual(kdaMetni(0, 0, 0), "0.00")})PS C:\kod\oyun-hesap> node --test kda.test.js✔ ölüm varsa KDA iki basamakla yazılır (6.2032ms)✔ hiç ölmeyen oyuncu Kusursuz sayılır (0.3138ms)✔ küsurat iki basamağa yuvarlanır (0.2143ms)✖ hiç katkısı olmayan oyuncu 0.00 alır (1.385ms)... ✖ failing tests: test at kda.test.js:17:1✖ hiç katkısı olmayan oyuncu 0.00 alır (1.385ms) AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: 'Kusursuz' !== '0.00' at TestContext.<anonymous> (file:///C:/kod/oyun-hesap/kda.test.js:18:12) ...Düzeltme, ölüm denetiminden önce katkıya bakmak:
export function kdaMetni(kill, olum, asist) { if (kill + asist === 0) { return "0.00" } if (olum === 0) { return "Kusursuz" } return ((kill + asist) / olum).toFixed(2)}PS C:\kod\oyun-hesap> npm test > oyun-hesap@1.0.0 test> node --test ▶ altinPaylas ✔ artanı kimseye vermez (2.5441ms) ✔ tam bölünürse artan 0 olur (0.2084ms) ✔ altın kişi sayısından azsa herkese 0 düşer (0.2138ms) ✔ hiç altın yoksa ikisi de 0 olur (0.1853ms) ✔ oyuncu yoksa hata fırlatır (0.5795ms)✔ altinPaylas (5.5516ms)✔ ölüm varsa KDA iki basamakla yazılır (1.095ms)✔ hiç ölmeyen oyuncu Kusursuz sayılır (0.1733ms)✔ küsurat iki basamağa yuvarlanır (0.139ms)✔ hiç katkısı olmayan oyuncu 0.00 alır (0.156ms)ℹ tests 9ℹ suites 1ℹ pass 9ℹ fail 0ℹ cancelled 0ℹ skipped 0ℹ todo 0ℹ duration_ms 137.842Dokuzu da yeşil. Bu son çalıştırmanın asıl söylediği şu: yeni if'i eklerken eski davranışlardan hiçbirini bozmadın. "Güvenle değiştirmek" tam olarak bu cümle.
Önce test (TDD) denemesi
Şimdiye kadar önce kodu yazdın, sonra testini. Bazı yazılımcılar sırayı tersine çevirir: önce testi yazar, testin kaldığını görür, sonra testi geçirecek kadar kod yazar. Bu yönteme teste dayalı geliştirme denir.
Neden testin önce kaldığını görmek gerekiyor? Çünkü hiç kalmayan bir test hiçbir şey kanıtlamaz. Yanlış dosyayı import ediyor ya da hiçbir şeyi denetlemiyor olabilir; kırmızıyı görmek testin gerçekten çalıştığının kanıtıdır.
Deneme için TFT'deki faiz: elindeki her 10 altın için tur sonunda 1 altın faiz alırsın, ama faiz en fazla 5 olur. Kuralı Ders 14'teki faiz görevinden tanıyorsun; bu kez onu sıfırdan, adım adım ve her adımda önce testini yazarak kuracaksın. Bu projede henüz faiz.js yok. Önce yalnız ilk davranışın testini yaz, faiz.test.js:
import { test } from "node:test"import assert from "node:assert/strict"import { faiz } from "./faiz.js" test("10 altının altında faiz yok", () => { assert.strictEqual(faiz(0), 0) assert.strictEqual(faiz(9), 0)})Kırmızı. Çalıştır. Daha dosya bile olmadığı için test, fonksiyona ulaşamadan kalır:
PS C:\kod\oyun-hesap> node --test faiz.test.jsnode:internal/modules/esm/resolve:272 throw new ERR_MODULE_NOT_FOUND( ^ Error [ERR_MODULE_NOT_FOUND]: Cannot find module 'C:\kod\oyun-hesap\faiz.js' imported from C:\kod\oyun-hesap\faiz.test.js ...✖ faiz.test.js (51.1212ms)ℹ tests 1ℹ suites 0ℹ pass 0ℹ fail 1...Yeşil. Testi geçirecek en az kodu yaz. Gerçekten en azı:
export function faiz(altin) { return 0}PS C:\kod\oyun-hesap> node --test faiz.test.js✔ 10 altının altında faiz yok (0.5859ms)ℹ tests 1ℹ suites 0ℹ pass 1ℹ fail 0...Her zaman 0 döndüren bir fonksiyon "hile" gibi görünüyor, öyle de. Ama bu hile bir şey söylüyor: testlerin henüz faizin artmasını istemiyor. İkinci davranışı test olarak ekle:
test("her 10 altın için 1 faiz", () => { assert.strictEqual(faiz(10), 1) assert.strictEqual(faiz(25), 2) assert.strictEqual(faiz(49), 4)})PS C:\kod\oyun-hesap> node --test faiz.test.js✔ 10 altının altında faiz yok (0.6033ms)✖ her 10 altın için 1 faiz (0.6397ms)... test at faiz.test.js:10:1✖ her 10 altın için 1 faiz (0.6397ms) AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: 0 !== 1 at TestContext.<anonymous> (file:///C:/kod/oyun-hesap/faiz.test.js:11:12) ...Kırmızı. Dikkat: rapor yalnızca ilk tutmayan satırı (11. satır, faiz(10)) gösteriyor. Bir test içindeki assert'lerden biri tutmayınca o testin kalanı çalışmaz. Şimdi yeşile çevir:
export function faiz(altin) { return Math.floor(altin / 10)}PS C:\kod\oyun-hesap> node --test faiz.test.js✔ 10 altının altında faiz yok (0.6175ms)✔ her 10 altın için 1 faiz (0.2019ms)ℹ tests 2ℹ suites 0ℹ pass 2ℹ fail 0...Sıradaki davranış, üst sınır:
test("faiz en fazla 5 olur", () => { assert.strictEqual(faiz(50), 5) assert.strictEqual(faiz(80), 5)})PS C:\kod\oyun-hesap> node --test faiz.test.js✔ 10 altının altında faiz yok (0.6388ms)✔ her 10 altın için 1 faiz (0.1016ms)✖ faiz en fazla 5 olur (0.6518ms)... test at faiz.test.js:16:1✖ faiz en fazla 5 olur (0.6518ms) AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: 8 !== 5 at TestContext.<anonymous> (file:///C:/kod/oyun-hesap/faiz.test.js:18:12) ...faiz(50) zaten 5 verdiği için geçti; tutmayan 18. satır, faiz(80): 8 geldi. Math.min iki sayıdan küçüğünü verir:
export function faiz(altin) { return Math.min(5, Math.floor(altin / 10))}PS C:\kod\oyun-hesap> node --test faiz.test.js✔ 10 altının altında faiz yok (0.6361ms)✔ her 10 altın için 1 faiz (0.1052ms)✔ faiz en fazla 5 olur (0.1394ms)ℹ tests 3ℹ suites 0ℹ pass 3ℹ fail 0...Düzenle. Kod çalışıyor ama 10 ve 5 sayılarının ne olduğu ilk bakışta belli değil. Testler yeşilken onlara ad ver:
const faizAdimi = 10const enFazlaFaiz = 5 export function faiz(altin) { const hamFaiz = Math.floor(altin / faizAdimi) return Math.min(enFazlaFaiz, hamFaiz)}node --test faiz.test.js yine üç yeşil verir. Düzenleme adımında davranış değişmez, yalnız kodun okunuşu değişir; testler bunun böyle kaldığını garanti eder. Gerçek işte TDD'yi her satırda değil, kuralları net olan hesaplarda kullanırsın. Bu denemeden aklında kalması gereken alışkanlık: yeni bir testi kırmızıyken en az bir kez gör.
Asenkron fonksiyonları test etmek
Bazı fonksiyonlar sonucu hemen değil, bir Promise olarak verir: dosya okumak, API'ye istek atmak, veritabanına sormak (Ders 15). Örnek bir maç geçmişini dosyadan okuyan maclar.js:
import { readFile } from "node:fs/promises" export async function maclariOku(dosyaAdi) { const metin = await readFile(dosyaAdi, "utf8") return JSON.parse(metin)}Yanına ornek-maclar.json:
[ { "sampiyon": "Ahri", "skor": "7/2/9", "kazandi": true }, { "sampiyon": "Lux", "skor": "3/5/12", "kazandi": false }, { "sampiyon": "Jinx", "skor": "10/0/6", "kazandi": true }]Test fonksiyonu da async olabilir. Test çalıştırıcı, döndürdüğü Promise bitene kadar bekler:
import { test } from "node:test"import assert from "node:assert/strict"import { maclariOku } from "./maclar.js" test("örnek dosyada üç maç var", async () => { const maclar = await maclariOku("ornek-maclar.json") assert.strictEqual(maclar.length, 3) assert.strictEqual(maclar[0].sampiyon, "Ahri")}) test("olmayan dosya hata verir", async () => { await assert.rejects(maclariOku("yok.json"), { code: "ENOENT" })})PS C:\kod\oyun-hesap> node --test maclar.test.js✔ örnek dosyada üç maç var (4.0976ms)✔ olmayan dosya hata verir (1.5192ms)ℹ tests 2ℹ suites 0ℹ pass 2ℹ fail 0...assert.rejects, assert.throws'un Promise'li hâli: "bu Promise reddedilmeli." İkinci parametre hatanın code alanına bakıyor; olmayan dosyada Node ENOENT verir (Ders 17). assert.rejects de bir Promise döndürür, o yüzden önünde await var. Bu derste iki await'in ikisini de unutmak mümkün ve iki farklı, şaşırtıcı sonuç veriyor:
undefined !== 3: testte await unutmakNe zaman çıkar? Asenkron fonksiyonun sonucunu await yazmadan kutuya koyup içine bakınca.
test("örnek dosyada üç maç var", async () => { const maclar = maclariOku("ornek-maclar.json") assert.strictEqual(maclar.length, 3) assert.strictEqual(maclar[0].sampiyon, "Ahri")})PS C:\kod\oyun-hesap> node --test maclar.test.js✖ örnek dosyada üç maç var (1.7502ms)✔ olmayan dosya hata verir (3.8436ms)... test at maclar.test.js:5:1✖ örnek dosyada üç maç var (1.7502ms) AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: undefined !== 3 at TestContext.<anonymous> (file:///C:/kod/oyun-hesap/maclar.test.js:7:12) ...maclar kutusunda dizi değil, bekleyen bir Promise var; Promise'in length alanı olmadığı için undefined geldi. Ders 15'teki Promise { <pending> } ile aynı hata, yalnız bu kez bir testin içinde.
Nasıl düzelir? Asenkron fonksiyonun önüne await, test fonksiyonunun önüne async yaz. Bir değerin undefined çıkması beklenmedik geldiyse ilk şüphen eksik await olsun.
İkincisi daha sinsi. Diyelim ki biri maclariOku'yu "dosya yoksa çökmesin" diye değiştirdi ve artık hata yerine boş dizi döndürüyor:
import { readFile } from "node:fs/promises" export async function maclariOku(dosyaAdi) { try { const metin = await readFile(dosyaAdi, "utf8") return JSON.parse(metin) } catch { return [] }}Testin yerinde, await'iyle duruyorsa durum açık: Missing expected rejection. ("beklenen ret gelmedi") der ve kalır. Asıl tehlike, assert.rejects'in önündeki await unutulmuşsa:
generated asynchronous activity after the test endedNe zaman çıkar? assert.rejects gibi Promise döndüren bir denetimin önüne await yazmayınca. Test, denetim bitmeden "geçti" diye kapanır; denetimin sonucu sonradan gelir.
test("olmayan dosya hata verir", async () => { assert.rejects(maclariOku("yok.json"), { code: "ENOENT" })})PS C:\kod\oyun-hesap> node --test maclar.test.js✔ örnek dosyada üç maç var (3.5427ms)✔ olmayan dosya hata verir (0.3168ms)ℹ Error: Test "olmayan dosya hata verir" at maclar.test.js:11:1 generated asynchronous activity after the test ended. This activity created the error "AssertionError [ERR_ASSERTION]: Missing expected rejection." and would have caused the test to fail, but instead triggered an unhandledRejection event.✖ maclar.test.js (64.7913ms)ℹ tests 3ℹ suites 0ℹ pass 2ℹ fail 1...Testin satırında ✔ var. Ardından gelen uzun ℹ Error satırı (sağa kaydırarak okuyabilirsin) ise "bu test bittikten sonra bir şey oldu, olsaydı test kalacaktı" diyor. Node sonuçta dosyayı kaldı sayıyor (fail 1) ama kalan şey testin adı değil, dosyanın adı. Hızlı bakan biri ✔'yi görüp geçer.
Nasıl düzelir? await assert.rejects(...) yaz. Kural: Promise döndüren her şeyin önünde, testte de await olmalı. Sonra maclar.js'i try'sız hâline geri al.
React bileşen testine ilk bakış
node:test sade JavaScript fonksiyonları için yeterli. Bir React bileşeni ise JSX ve TypeScript içerir, çalışmak için de bir sayfaya (DOM) ihtiyaç duyar; Node'da sayfa yok. Bunun için iki araç birlikte kullanılır:
- Vitest: Vite ile aynı ekibin test çalıştırıcısı. Projenin
vite.config.ts'ini okur;.tsxdosyalarını Vite'ın çevirdiği gibi çevirir. - Testing Library: bileşeni sahte bir sayfaya çizen ve içinde, kullanıcının gördüğü yazılara ve düğmelere göre arama yapan kütüphane.
Ders 41'deki tft-kurucu projesini aç (C:\kod\tft-kurucu) ve terminalde kur. Hepsi geliştirme aracı olduğu için -D ile (geliştirme bağımlılığı):
PS C:\kod\tft-kurucu> npm install -D vitest @testing-library/react @testing-library/dom @testing-library/user-event jsdom added 71 packages, and audited 99 packages in 5s 22 packages are looking for funding run `npm fund` for details found 0 vulnerabilitiesBu ders Vitest 5.0.3, @testing-library/react 16.3.3 ve jsdom 30.1.2 ile denendi (12 Ekim 2026); sende daha yeni sürümler kurulabilir. @testing-library/user-event tıklama ve yazma gibi kullanıcı hareketlerini taklit eder; jsdom ise Node'un içinde çalışan sahte bir tarayıcı sayfasıdır.
İlk Vitest testi
Önce sade bir fonksiyon: birimler.ts'deki kopyaSayisi. src klasöründe birimler.test.ts aç:
import { describe, it, expect } from "vitest"import { kopyaSayisi } from "./birimler.ts" describe("kopyaSayisi", () => { it("bir yıldız 1 kopyadır", () => { expect(kopyaSayisi(1)).toBe(1) }) it("iki yıldız 3, üç yıldız 9 kopyadır", () => { expect(kopyaSayisi(2)).toBe(3) expect(kopyaSayisi(3)).toBe(9) })})describe ve it tanıdık. Değişen tek şey denetim cümlesi: assert.strictEqual(gercek, beklenen) yerine expect(gercek).toBe(beklenen). Okunuşu "gerçeğin, beklenen olmasını beklerim". Objeler için toEqual (deepEqual'ın karşılığı) var.
PS C:\kod\tft-kurucu> npx vitest run RUN v5.0.3 C:/kod/tft-kurucu ✓ src/birimler.test.ts (2 tests) 3ms Test Files 1 passed (1) Tests 2 passed (2) Start at 04:02:29 Duration 554ms (worker 36%, transform 32%, import 28%, tests 3%)Rapor node:test'inkinden farklı görünüyor ama okunuşu benzer: ✓ geçen test dosyası, parantezde içindeki test sayısı; altta özet. Start at çalıştırmanın saati, Duration toplam süre; ikisi de sende farklı çıkar.
vitest run testleri bir kez çalıştırıp çıkar. Yalnız npx vitest yazarsan izleme kipinde açılır: dosya kaydettikçe testleri yeniden çalıştırır, q ile çıkarsın. Vitest de adında .test. ya da .spec. geçen dosyaları arar.
Bileşeni çizip içine bakmak
Şimdi asıl konu: TakimPaneli. Ders 41'de elle denediğin üç davranışı teste yazacaksın: takım boşken bilgi yazısı, altın yetmezken uyarı ve Çıkar düğmesinin doğru birimi bildirmesi. src/TakimPaneli.test.tsx:
import { describe, it, expect, vi } from "vitest"import { render, screen } from "@testing-library/react"import userEvent from "@testing-library/user-event"import TakimPaneli from "./TakimPaneli.tsx" const lux = { id: 1, ad: "Lux", maliyet: 3, yildiz: 2 } describe("TakimPaneli", () => { it("takım boşken bilgi yazısı gösterir", () => { render( <TakimPaneli takim={[]} altin={20} seviye={3} onCikar={vi.fn()} onYildiz={vi.fn()} /> ) expect(screen.getByText("Takım boş. Dükkândan birim ekle.")).toBeTruthy() }) it("altın yetmezse uyarı gösterir", () => { render( <TakimPaneli takim={[lux]} altin={5} seviye={3} onCikar={vi.fn()} onYildiz={vi.fn()} /> ) expect(screen.getByText("Altının yetmiyor: 4 altın eksik.")).toBeTruthy() }) it("Çıkar düğmesi birimin id'sini bildirir", async () => { const onCikar = vi.fn() render( <TakimPaneli takim={[lux]} altin={20} seviye={3} onCikar={onCikar} onYildiz={vi.fn()} /> ) await userEvent.click(screen.getByRole("button", { name: "Çıkar" })) expect(onCikar).toHaveBeenCalledWith(1) })})Yeni parçalar:
render(<TakimPaneli … />): bileşeni sahte sayfaya çizer. Props'ları Ders 41'dekiAppnasıl veriyorsa öyle veriyorsun.screen.getByText("…"): sayfada bu yazıyı taşıyan öğeyi bulur. Bulamazsa hata fırlatır, yani bulmak başlı başına bir denetimdir;toBeTruthy()yalnız okunuşu tamamlar.screen.getByRole("button", { name: "Çıkar" }): üzerinde "Çıkar" yazan düğmeyi bulur. Rol, öğenin ne olduğunu söyler (düğme, başlık, metin kutusu). Ekran okuyucular da sayfayı bu rollerle tanır; Ders 45'teki erişilebilirlik konusuyla aynı fikir.vi.fn(): çağrıldığında hiçbir şey yapmayan ama her çağrıyı kaydeden sahte bir fonksiyon. Ders 50'deki sahte nesnenin küçük kardeşi.toHaveBeenCalledWith(1)"bu fonksiyon 1 ile çağrıldı mı" diye sorar.await userEvent.click(...): gerçek bir tıklamayı taklit eder. Promise döndürür;await'i unutma.
Lux 2 yıldızlı, yani 3 kopya: toplam maliyet 9. Altın 5 olduğunda 4 eksik; ikinci test bu yüzden "4 altın eksik" bekliyor. Testlerin hiçbiri CSS sınıfına ya da bileşenin içindeki değişken adlarına bakmıyor. Kullanıcının gördüğüne bakıyor; bileşenin içini yeniden yazsan da kullanıcı aynı şeyi görüyorsa testler geçer.
Çalıştır:
ReferenceError: document is not definedNe zaman çıkar? Bir bileşen testini, Vitest'e sahte sayfa kullanmasını söylemeden çalıştırınca. Vitest varsayılan olarak testleri düz Node ortamında çalıştırır; Node'da tarayıcıdaki document yoktur.
PS C:\kod\tft-kurucu> npx vitest run RUN v5.0.3 C:/kod/tft-kurucu ✓ src/birimler.test.ts (2 tests) 3ms ❯ src/TakimPaneli.test.tsx (3 tests | 3 failed) 6ms ❯ TakimPaneli (3) × takım boşken bilgi yazısı gösterir 4ms × altın yetmezse uyarı gösterir 0ms × Çıkar düğmesi birimin id'sini bildirir 0ms ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 3 ⎯⎯⎯⎯⎯⎯⎯ FAIL src/TakimPaneli.test.tsx > TakimPaneli > takım boşken bilgi yazısı gösterirReferenceError: document is not defined ❯ render node_modules/@testing-library/react/dist/pure.js:265:5 ❯ src/TakimPaneli.test.tsx:10:9 8| describe("TakimPaneli", () => { 9| it("takım boşken bilgi yazısı gösterir", () => { 10| render( | ^ 11| <TakimPaneli 12| takim={[]}...Kalan testlerde Vitest'in raporu da node:test'inki gibi aynı soruları cevaplıyor: hangi dosya, hangi test (× kalan, ✓ geçen), hangi hata, hangi satır. Kodun ilgili satırını da ^ ile işaretleyerek gösteriyor.
Nasıl düzelir? vite.config.ts'e test bölümünü ekle (hemen aşağıda).
vite.config.ts'i aç. Bu dosya Vite şablonundan geldiği için tek tırnak ve 2 boşluk girinti kullanıyor; kendi stiline çevirmene gerek yok, yalnız işaretli satırları ekle:
/// <reference types="vitest/config" />import react from '@vitejs/plugin-react'import { defineConfig } from 'vite' // https://vite.dev/config/export default defineConfig({ plugins: [react()], test: { environment: 'jsdom', globals: true, },})/// <reference types="vitest/config" />: TypeScript'e "bu dosyadatestdiye bir bölüm olabilir" der. Yazmazsannpm run buildsırasındatscbu bölüme'test' does not exist in type 'UserConfigExport'diye itiraz eder.environment: 'jsdom': testleri jsdom'un sahte sayfasında çalıştır;documentartık var.globals: true: Vitest'indescribe,it,afterEachgibi adlarını her yerden erişilebilir yapar. Testing Library her testten sonra çizdiği bileşeni sayfadan silmek için genel birafterEacharar; bulamazsa silmez. Bu satırı unutursan ne olacağı hemen aşağıdaki kartta.
Son olarak test betiğini ayarla ve çalıştır:
PS C:\kod\tft-kurucu> npm pkg set scripts.test="vitest run"PS C:\kod\tft-kurucu> npm test > tft-kurucu@0.0.0 test> vitest run RUN v5.0.3 C:/kod/tft-kurucu ✓ src/birimler.test.ts (2 tests) 3ms ✓ src/TakimPaneli.test.tsx (3 tests) 153ms Test Files 2 passed (2) Tests 5 passed (5) Start at 04:03:00 Duration 1.43s (environment 77%, import 11%, tests 7%, transform 3%, worker 2%)İlk çalıştırmada jsdom'un yüklenmesi birkaç saniye sürebilir; sonrakiler hızlanır.
TestingLibraryElementError: Found multiple elements with the role "button" and name "Çıkar"Ne zaman çıkar? vite.config.ts'te globals: true yokken, aynı dosyada birden çok testin bileşen çizdiği durumda. Önceki testlerin bileşenleri sayfada kalır; üçüncü test iki tane "Çıkar" düğmesi bulur.
PS C:\kod\tft-kurucu> npx vitest run RUN v5.0.3 C:/kod/tft-kurucu ✓ src/birimler.test.ts (2 tests) 4ms ❯ src/TakimPaneli.test.tsx (3 tests | 1 failed) 134ms ❯ TakimPaneli (3) ✓ takım boşken bilgi yazısı gösterir 24ms ✓ altın yetmezse uyarı gösterir 5ms × Çıkar düğmesi birimin id'sini bildirir 104ms ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯ FAIL src/TakimPaneli.test.tsx > TakimPaneli > Çıkar düğmesi birimin id'sini bildirirTestingLibraryElementError: Found multiple elements with the role "button" and name "Çıkar" Here are the matching elements:...Mesajın devamında Testing Library sayfanın o anki HTML'ini döker; orada üç ayrı <section class="panel"> görürsün: her testin çizdiği panel üst üste birikmiş.
Nasıl düzelir? vite.config.ts'teki test bölümüne globals: true ekle. Gerçekten birden çok öğe bekliyorsan getAllByRole kullan; mesajın kendisi de bunu öneriyor.
ESLint ve Prettier: kod kalitesinin iki bekçisi
Testler "kod doğru sonucu veriyor mu" diye sorar. İki soru daha var: "kodda şüpheli bir kalıp var mı?" ve "kod herkesin okuyabileceği tek bir biçimde mi?" Bunlara iki ayrı araç bakar.
ESLint
oyun-hesap'a dön. Bu projede ilk kez paket kuracaksın; önce klasörün köküne .gitignore adında bir dosya aç ve içine tek satır yaz (Ders 14):
node_modulesSonra ESLint'i kur. Bu ders ESLint 10.12.0 ile denendi:
PS C:\kod\oyun-hesap> npm install --save-dev eslint@latest @eslint/js@latest globals@latest added 79 packages, and audited 80 packages in 4s 18 packages are looking for funding run `npm fund` for details found 0 vulnerabilitiesESLint'in ayarları klasörün kökündeki eslint.config.js dosyasında durur. Bu, ESLint'in kendi belgesindeki kalıbın Node'a uyarlanmış hâli:
import { defineConfig } from "eslint/config"import js from "@eslint/js"import globals from "globals" export default defineConfig([ { files: ["**/*.js"], plugins: { js }, extends: ["js/recommended"], languageOptions: { globals: globals.node } }])files: ["**/*.js"]: bütün klasörlerdeki.jsdosyalarına uygula.node_modules'a ESLint zaten bakmaz.extends: ["js/recommended"]: ESLint ekibinin önerdiği kural takımını aç.globals: globals.node: Node'da tanımlı olanconsole,processgibi adları tanıt.globalspaketi bu adların listesini taşır.
Şimdi bilerek dağınık bir dosya yaz, rapor.js. Her maçın KDA'sını yazacak ama içinde dört kusur var:
import { kdaMetni } from "./kda.js"import { faiz } from "./faiz.js" const maclar = [ { sampiyon: "Ahri", kill: 7, olum: 2, asist: 9 }, { sampiyon: "Jinx", kill: 10, olum: 0, asist: 6 }] for (const mac of maclar) { if (mac.olum = 0) { console.log(mac.sampiyon + " hiç ölmedi") } console.log(mac.sampiyon + ": " + kdaMetni(mac.kill, mac.olum, mac.asit))}console.log("Toplam maç: " + macSayisi)PS C:\kod\oyun-hesap> npx eslint . C:\kod\oyun-hesap\rapor.js 2:10 error 'faiz' is defined but never used no-unused-vars 10:9 error Expected a conditional expression and instead saw an assignment no-cond-assign 10:9 error Unexpected constant condition no-constant-condition 15:30 error 'macSayisi' is not defined no-undef ✖ 4 problems (4 errors, 0 warnings)Her satır: satır:sütun, önem (error ya da warning), açıklama ve en sağda kuralın adı. Kuralın adını aratırsan ESLint'in sitesinde örnekli bir sayfa bulursun.
| Kural | Ne diyor | Düzeltme |
|---|---|---|
no-unused-vars | faiz alındı ama hiç kullanılmadı | Satırı sil |
no-cond-assign | if içinde karşılaştırma yerine atama (=) var | === yaz (Ders 2) |
no-constant-condition | mac.olum = 0 her zaman 0, yani koşul hep yanlış | Aynı düzeltme |
no-undef | macSayisi diye bir kutu yok | maclar.length |
if (mac.olum = 0) satırını hatırla: Ders 2'de bu hatanın mesaj vermeden yanlış çalıştığını, yani bir mantık hatası olduğunu görmüştün. Node hiç uyarmaz; ESLint kodu çalıştırmadan yakaladı. Dört kusuru da düzelt:
import { kdaMetni } from "./kda.js" const maclar = [ { sampiyon: "Ahri", kill: 7, olum: 2, asist: 9 }, { sampiyon: "Jinx", kill: 10, olum: 0, asist: 6 }] for (const mac of maclar) { if (mac.olum === 0) { console.log(mac.sampiyon + " hiç ölmedi") } console.log(mac.sampiyon + ": " + kdaMetni(mac.kill, mac.olum, mac.asit))}console.log("Toplam maç: " + maclar.length)PS C:\kod\oyun-hesap> npx eslint .PS C:\kod\oyun-hesap> node rapor.jsAhri: NaNJinx hiç ölmediJinx: KusursuzToplam maç: 2ESLint artık hiçbir şey söylemiyor (sessizlik "sorun yok" demek), ama çıktı yanlış: Ahri: NaN. 12. satırdaki mac.asit yazım hatası, ESLint'in göremeyeceği türden bir hata; obje alanlarının adını bilmez. Jinx'te Kusursuz doğru görünüyor ama o da şans: ölüm 0 olduğu için hesaba hiç girilmedi. asit'i asist yap:
PS C:\kod\oyun-hesap> node rapor.jsAhri: 8.00Jinx hiç ölmediJinx: KusursuzToplam maç: 2Bu küçük olay, araçların neden birlikte kullanıldığını gösteriyor:
| Araç | Neyi yakalar | Bu dosyada |
|---|---|---|
| Testler | Yanlış sonuç | rapor.js için test olsaydı NaN'ı yakalardı |
| ESLint | Şüpheli kalıp, tanımsız ad | = yerine ===, macSayisi, kullanılmayan import |
| TypeScript | Yanlış tip, olmayan alan | mac.asit'i yazarken altını çizerdi (Ders 39) |
| Prettier | Dağınık biçim | Birazdan |
'console' is not defined no-undefNe zaman çıkar? eslint.config.js'te languageOptions.globals bölümünü unutunca. ESLint varsayılan olarak yalnızca JavaScript dilinin kendi adlarını tanır; console ise Node'un (ve tarayıcının) eklediği bir addır. Düzeltilmiş rapor.js'te bile her console.log satırı işaretlenir:
PS C:\kod\oyun-hesap> npx eslint . C:\kod\oyun-hesap\rapor.js 10:9 error 'console' is not defined no-undef 12:5 error 'console' is not defined no-undef 14:1 error 'console' is not defined no-undef ✖ 3 problems (3 errors, 0 warnings)Nasıl düzelir? import globals from "globals" satırını ve languageOptions: { globals: globals.node } bölümünü ekle. Tarayıcıda çalışan bir projede globals.browser yazarsın.
Prettier
Kur. --save-exact sürümü tam olarak sabitler; biçimlendiricinin sürümü değişince biçim de değişebileceği için Prettier'ın kendi belgesi bunu öneriyor:
PS C:\kod\oyun-hesap> npm install --save-dev --save-exact prettier added 1 package, and audited 81 packages in 900ms 19 packages are looking for funding run `npm fund` for details found 0 vulnerabilitiesAyarları .prettierrc adlı bir JSON dosyasına yaz. Prettier'ın varsayılanları noktalı virgül ve 2 boşluk; bu dört satır onu kursun stiline uyduruyor:
{ "semi": false, "tabWidth": 4, "trailingComma": "none", "endOfLine": "auto"}semi: false: satır sonuna noktalı virgül koyma.tabWidth: 4: girinti 4 boşluk.trailingComma: "none": çok satırlı obje ve dizilerin son elemanından sonra virgül koyma.endOfLine: "auto": satır sonlarına dokunma. Windows'ta VS Code satırları başka, npm başka işaretle bitirir (Ders 14'tekiLF will be replaced by CRLFuyarısı bundandı). Bu satır olmazsa Prettier VS Code'da yazdığın her dosyayı "biçimsiz" sayar.
Bir de .prettierignore dosyası aç. İçine yazdığın dosyalara Prettier dokunmaz:
package.jsonpackage-lock.jsonBu iki dosyayı npm kendisi yazıyor; Prettier'ın onları 4 boşluğa çevirmesi, npm'in her seferinde 2'ye geri çevirmesi gereksiz bir çekişme olur. Prettier .gitignore'daki dosyaları da kendiliğinden atlar.
Şimdi bir ekip arkadaşının aceleyle yazdığı bir dosya düşün, seviye.js:
export function seviyeHesapla(xp){ const seviye=Math.floor(xp/100)+1; if(seviye>18){return 18} return seviye;}Çalışıyor ama okuması yorucu. Önce denetle, sonra düzelt:
PS C:\kod\oyun-hesap> npx prettier . --checkChecking formatting...[warn] seviye.js[warn] Code style issues found in the above file. Run Prettier with --write to fix.PS C:\kod\oyun-hesap> npx prettier . --write.prettierrc 18ms (unchanged)altin.js 9ms (unchanged)altin.test.js 8ms (unchanged)eslint.config.js 2ms (unchanged)faiz.js 2ms (unchanged)faiz.test.js 3ms (unchanged)kda.js 1ms (unchanged)kda.test.js 3ms (unchanged)maclar.js 2ms (unchanged)maclar.test.js 3ms (unchanged)ornek-maclar.json 1ms (unchanged)rapor.js 2ms (unchanged)seviye.js 1ms--check yalnızca bakar ve biçimsiz dosyaları listeler; --write onları yeniden yazar. (unchanged) yazanlara dokunmadı: bu kursta yazdığın kod zaten bu biçimde. Yalnızca seviye.js değişti:
export function seviyeHesapla(xp) { const seviye = Math.floor(xp / 100) + 1 if (seviye > 18) { return 18 } return seviye}Prettier anlamı değiştirmedi; yalnızca boşlukları, girintiyi ve satırları düzenledi, noktalı virgülleri attı. Ders 7'de "çoğu ekip bunu bir araçla kendiliğinden düzenler" demiştik: o araç bu.
Komutları betiğe bağla
Üç komutu her seferinde hatırlamak yerine package.json'a betik olarak yaz. Adlar dünyada yaygın olanlar; başka bir projede de npm run lint yazınca ne olacağını bilirsin:
PS C:\kod\oyun-hesap> npm pkg set scripts.lint="eslint ." scripts.format="prettier . --write" scripts.format:check="prettier . --check"PS C:\kod\oyun-hesap> npm run lint > oyun-hesap@1.0.0 lint> eslint . PS C:\kod\oyun-hesap> npm run format:check > oyun-hesap@1.0.0 format:check> prettier . --check Checking formatting...All matched files use Prettier code style!package.json'daki scripts bölümü artık şöyle:
"scripts": { "test": "node --test", "lint": "eslint .", "format": "prettier . --write", "format:check": "prettier . --check" },GitHub Actions: her push'ta test
Testlerin, ESLint'in ve Prettier'ın bir zayıf noktası var: çalıştırmayı hatırlaman gerekiyor. Yorgun bir akşam unutursun, ekip arkadaşın hiç kurmamıştır. Çözüm, bu komutları senin bilgisayarında değil, GitHub'da, her gönderimde kendiliğinden çalıştırmak.
GitHub'ın CI hizmetinin adı GitHub Actions. Ona ne yapacağını, deponun içindeki bir dosyayla anlatırsın.
YAML'a hızlı bakış
YAML, JSON gibi bir veri biçimi ama süslü parantez ve tırnak yerine girinti kullanır. Üç kural yeter:
ad: değerbir alan. Değer metinse tırnağa genelde gerek yok.- Bir alanın altındaki alanlar daha içeride yazılır. Girinti boşlukla olur, sekmeyle olmaz. Aynı seviyedekiler aynı sütunda başlamalı.
-ile başlayan satırlar bir listenin elemanları.
JavaScript'te girinti yalnızca okunuş içindi; YAML'da ise anlamın kendisi. Bir satırı bir boşluk kaydırmak, o alanı başka bir yere taşır.
İş akışı dosyası
oyun-hesap'ta önce .github klasörünü, onun içinde workflows klasörünü, onun içinde de test.yml dosyasını oluştur. VS Code'da New File'a .github/workflows/test.yml yazarsan klasörleri kendisi açar. Dosyanın tamamı:
name: Testler on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - uses: actions/setup-node@v7 with: node-version: 24 - run: npm ci - run: npm run lint - run: npm run format:check - run: npm testSitede YAML renklendirmesi yok, o yüzden düz metin gibi görünüyor; VS Code'da renkli görürsün. Girintiyi Prettier'ın 4 boşluklu biçimiyle yazdık, npm run format:check bu dosyayı da denetliyor.
Satır satır:
name: Testler: iş akışının adı; Actions sekmesinde bu adla görünür.on: [push, pull_request]: ne zaman çalışsın? Her push'ta ve her pull request'te (Ders 52). Köşeli parantez YAML'da kısa liste yazımıdır.jobs:altında bir iş var, adıtest.runs-on: ubuntu-latest: GitHub bu iş için Ubuntu (Linux) yüklü, temiz, geçici bir bilgisayar açar. Senin Windows'unla ilgisi yok; iş bitince silinir.steps:adımlar sırayla çalışır.uses:başkasının hazırladığı bir adımı kullanır,run:bir terminal komutu çalıştırır.actions/checkout@v7: deponun kodunu o geçici bilgisayara indirir.@v7kullanılan sürüm.actions/setup-node@v7: Node kurar.with:ona ayar verir;node-version: 24.with'inusesile aynı sütunda başladığına dikkat et: ikisi de aynı listenin aynı elemanına ait.npm ci: paketleri kurar (aşağıda).- Son üç adım senin betiklerin. Biri başarısız olursa (çıkış kodu 0 değilse) sonraki adımlar çalışmaz ve iş kırmızı biter.
Bu ders actions/checkout ve actions/setup-node'un 7. ana sürümleriyle yazıldı (GitHub'daki sayfalarında Ekim 2026'daki en yeni sürüm). Daha yenisi çıkınca o eylemin GitHub sayfasındaki örneğe bak; @v7'yi @v8 yapmak genellikle yeter.
npm ci: kilit dosyasına sadık kurulum
CI'da npm install yerine npm ci kullanılır (clean install, temiz kurulum). node_modules'u silip yalnızca package-lock.json'da yazan sürümleri kurar; package.json'a ve kilit dosyasına dokunmaz. Böylece GitHub'daki bilgisayar, seninkiyle aynı paket sürümlerini kullanır. Bunun için kilit dosyasının Git'te olması ve package.json ile uyuşması şart.
npm ci: package.json ile package-lock.json uyuşmuyor (are in sync)Ne zaman çıkar? package.json ile package-lock.json birbirini tutmayınca. En sık sebepler: package.json'ı elle değiştirip npm install çalıştırmamak ya da kilit dosyasını Git'e hiç eklememek. Bilgisayarında aynı hatayı npm ci yazarak görebilirsin:
PS C:\kod\oyun-hesap> npm cinpm error code EUSAGEnpm errornpm error `npm ci` can only install packages when your package.json and package-lock.json or npm-shrinkwrap.json are in sync. Please update your lock file with `npm install` before continuing.npm errornpm error Missing: prettier@3.9.10 from lock filenpm errornpm error Clean install a project...Missing: prettier@3.9.10 from lock file: "package.json Prettier istiyor ama kilit dosyasında yok." Altında npm'in uzun bir kullanım kılavuzu gelir, onu geç.
Nasıl düzelir? Bilgisayarında npm install çalıştır (kilit dosyasını günceller), package-lock.json'ı commit'le ve yeniden push'la. Kilit dosyasını .gitignore'a yazdıysan oradan çıkar; Git'te olması gerekiyor. Hiç kilit dosyası yoksa mesaj başka: içinde can only install with an existing package-lock.json geçer.
Gönder ve izle
Şimdi her şeyi commit'le. oyun-hesap'ta git init'i dersin başında yapmıştın; bu, deponun ilk kaydı:
PS C:\kod\oyun-hesap> git add .warning: in the working copy of 'package-lock.json', LF will be replaced by CRLF the next time Git touches itwarning: in the working copy of 'package.json', LF will be replaced by CRLF the next time Git touches itPS C:\kod\oyun-hesap> git commit -m "Ders 53: testler, ESLint, Prettier ve GitHub Actions"[master (root-commit) 19923d7] Ders 53: testler, ESLint, Prettier ve GitHub Actions 18 files changed, 1220 insertions(+) create mode 100644 .github/workflows/test.yml create mode 100644 .gitignore create mode 100644 .prettierignore create mode 100644 .prettierrc create mode 100644 altin.js create mode 100644 altin.test.js create mode 100644 eslint.config.js create mode 100644 faiz.js create mode 100644 faiz.test.js create mode 100644 kda.js create mode 100644 kda.test.js create mode 100644 maclar.js create mode 100644 maclar.test.js create mode 100644 ornek-maclar.json create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 rapor.js create mode 100644 seviye.jsListede node_modules yok: ESLint'i kurmadan önce açtığın .gitignore işini yaptı. package-lock.json ise var; npm ci için gerekli. .github/workflows/test.yml de var; GitHub iş akışını oradan okuyacak.
LF will be replaced by CRLF uyarıları npm'in yazdığı iki dosya için çıkar, Ders 14'teki gibi zararsız. Kayıt kimliği ve satır sayısı sende farklı olur.
Sonra GitHub'a gönder. Depoyu GitHub'da Ders 7'deki gibi oyun-hesap adıyla, boş olarak (README eklemeden) oluştur ve şu üç komutu çalıştır; adresteki kullanıcı adı seninki olacak:
git remote add origin https://github.com/denizyilmaz/oyun-hesap.gitgit branch -M maingit push -u origin mainPush'un çıktısı Ders 7'dekine benzer; sonunda branch 'main' set up to track 'origin/main'. satırını görürsün. Push biter bitmez iş akışı başlar. GitHub'da deponun sayfasını aç:
- Deponun adının altındaki sekmelerden Actions'a tıkla. Solda iş akışlarının listesi var; Testler orada.
- Ortada çalıştırmaların listesi var: her satır bir push. Başında sarı dönen bir daire varsa hâlâ çalışıyor; yeşil tik geçti, kırmızı çarpı kaldı demek.
- Bir çalıştırmaya, sonra soldaki test işine tıkla. Her adım bir satır; bir adıma tıklayınca o adımın terminal çıktısı açılır. Kalan bir testte, bilgisayarında gördüğün
✖ failing testsraporunun aynısını burada görürsün.
Aynı tik ya da çarpı deponun ana sayfasında son commit'in yanında ve pull request sayfasının altında da görünür. GitHub'ın arayüzü zaman zaman değişir; sekmelerin yeri ya da adı biraz farklı olabilir, ama Actions sekmesi uzun süredir aynı yerde. Herkese açık depolarda GitHub'ın standart çalıştırıcıları ücretsiz; özel depolarda ücretsiz planda ayda 2.000 dakika var. Bu projenin bir çalıştırması bir dakikayı bulmaz.
Ekipler genelde bir adım daha atar: GitHub'da ana dal için "testler yeşil olmadan pull request birleştirilemesin" kuralı koyar. Böylece kırmızı kod ana dala hiç giremez. Bu ayar deponun Settings sayfasında; kendi projelerinde şimdilik gerek yok, iş yerinde göreceksin.
Görevler
Görev 1–5'i oyun-hesap'ta, Görev 6'yı tft-kurucu'da yap. Her görevde önce testi yaz ve bir kez kırmızı gör: kodu geçici olarak bozarak ya da testi koddan önce yazarak. Kırmızıyı hiç görmediğin bir teste güvenme.
Can hiç eksiye düşmesin
can.js adında bir dosyaya şu fonksiyonu yaz:
export function kalanCan(hp, hasar) { return Math.max(0, hp - hasar)}Sonra can.test.js dosyasına describe ve it ile dört test yaz: sıradan bir hasar, cana tam eşit hasar, candan büyük hasar ve 0 hasar. node --test can.test.js dört yeşil göstermeli.
Son adım: eksi hasar saçma bir girdi. kalanCan(100, -20) canı artırmasın, "Hasar eksi olamaz" mesajıyla hata fırlatsın. Önce bunun testini beşinci it olarak yaz ve kırmızıyı gör, sonra can.js'e kapıyı ekle.
İpucu 1
Kalıp altin.test.js ile aynı; yalnız dönen değer bir sayı, yani deepEqual yerine strictEqual.
İpucu 2
Dört testin bir şey yakaladığını görmek için Math.max(0, …)'ı geçici olarak sil ve yalnız return hp - hasar bırak. Hangi test kırmızı oldu?
İpucu 3
Hata testi altinPaylas'taki gibi: assert.throws(() => kalanCan(100, -20), { message: "Hasar eksi olamaz" }). Kapı da aynı biçimde: return'den önce if (hasar < 0) ve içinde throw new Error(...).
Çözümü göster
import { describe, it } from "node:test"import assert from "node:assert/strict"import { kalanCan } from "./can.js" describe("kalanCan", () => { it("hasarı candan düşer", () => { assert.strictEqual(kalanCan(100, 30), 70) }) it("hasar cana eşitse 0 kalır", () => { assert.strictEqual(kalanCan(100, 100), 0) }) it("hasar candan büyükse eksiye düşmez", () => { assert.strictEqual(kalanCan(100, 250), 0) }) it("hasar 0 ise can değişmez", () => { assert.strictEqual(kalanCan(100, 0), 100) }) it("eksi hasar hata fırlatır", () => { assert.throws(() => kalanCan(100, -20), { message: "Hasar eksi olamaz" }) })})Kapıyı eklemeden önce beşinci test kırmızı:
PS C:\kod\oyun-hesap> node --test can.test.js▶ kalanCan ✔ hasarı candan düşer (0.5865ms) ✔ hasar cana eşitse 0 kalır (0.1088ms) ✔ hasar candan büyükse eksiye düşmez (0.0998ms) ✔ hasar 0 ise can değişmez (0.1024ms) ✖ eksi hasar hata fırlatır (0.5605ms)✖ kalanCan (2.9487ms)... test at can.test.js:22:5✖ eksi hasar hata fırlatır (0.5605ms) AssertionError [ERR_ASSERTION]: Missing expected exception. at TestContext.<anonymous> (file:///C:/kod/oyun-hesap/can.test.js:23:16) ...can.js'in son hâli:
export function kalanCan(hp, hasar) { if (hasar < 0) { throw new Error("Hasar eksi olamaz") } return Math.max(0, hp - hasar)}PS C:\kod\oyun-hesap> node --test can.test.js▶ kalanCan ✔ hasarı candan düşer (0.6488ms) ✔ hasar cana eşitse 0 kalır (0.1203ms) ✔ hasar candan büyükse eksiye düşmez (0.1057ms) ✔ hasar 0 ise can değişmez (0.1052ms) ✔ eksi hasar hata fırlatır (0.5739ms)✔ kalanCan (2.9694ms)ℹ tests 5ℹ suites 1ℹ pass 5ℹ fail 0ℹ cancelled 0ℹ skipped 0ℹ todo 0ℹ duration_ms 68.6723Math.max'i silince yalnızca üçüncü test kalır (-150 !== 0): eksiye düşmeyi yakalayan tek test, sınırın ötesindeki değeri deneyen test. Cana tam eşit hasar (100, 100) iki sürümde de 0 verir ama yine de yazılmalı; biri ileride Math.max(1, …) yazarsa onu da o yakalar.
Lux'un son yeteneği
mana.js'teki fonksiyonun bir hatası var:
export function yetenekHazirMi(mana, bedel) { return mana > bedel}Lux'un 50 manası var, yeteneği 50 mana tutuyor: oyunda yeteneği kullanabilmeli. Hatayı önce bir testle göster (kırmızı), sonra düzelt (yeşil). mana.test.js'te en az üç test olsun: mana bedelden fazla, az ve tam eşit.
İpucu 1
Sınırın üç yanı: 100 ile 50, 49 ile 50, 50 ile 50. Fonksiyon true ya da false döndürüyor; strictEqual(…, true) yazabilirsin.
İpucu 2
Kırmızıyı görünce düzeltme tek karakter: "büyük ya da eşit".
Çözümü göster
import { describe, it } from "node:test"import assert from "node:assert/strict"import { yetenekHazirMi } from "./mana.js" describe("yetenekHazirMi", () => { it("mana bedelden fazlaysa hazır", () => { assert.strictEqual(yetenekHazirMi(100, 50), true) }) it("mana bedelden azsa hazır değil", () => { assert.strictEqual(yetenekHazirMi(49, 50), false) }) it("mana bedele tam eşitse hazır", () => { assert.strictEqual(yetenekHazirMi(50, 50), true) })})PS C:\kod\oyun-hesap> node --test mana.test.js▶ yetenekHazirMi ✔ mana bedelden fazlaysa hazır (0.5348ms) ✔ mana bedelden azsa hazır değil (0.7306ms) ✖ mana bedele tam eşitse hazır (0.8329ms)✖ yetenekHazirMi (3.0363ms)... test at mana.test.js:14:5✖ mana bedele tam eşitse hazır (0.8329ms) AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: false !== true at TestContext.<anonymous> (file:///C:/kod/oyun-hesap/mana.test.js:15:16) ...mana.js'te > yerine >= yaz:
export function yetenekHazirMi(mana, bedel) { return mana >= bedel}PS C:\kod\oyun-hesap> node --test mana.test.js▶ yetenekHazirMi ✔ mana bedelden fazlaysa hazır (0.5605ms) ✔ mana bedelden azsa hazır değil (0.5934ms) ✔ mana bedele tam eşitse hazır (0.1117ms)✔ yetenekHazirMi (2.1475ms)ℹ tests 3ℹ suites 1ℹ pass 3ℹ fail 0...İlk iki test hatalı kodda da geçiyordu. Hatayı yalnızca sınırın tam üstündeki değer yakaladı.
Galibiyet serisi altını, önce testle
TFT'de üst üste kazanınca ya da kaybedince seri altını alırsın. Bu görevde kural şu: seri 0 ya da 1 ise 0 altın; 2 ya da 3 ise 1 altın; 4 ise 2 altın; 5 ve üstü 3 altın. seriAltini(seri) fonksiyonunu TDD ile yaz: seri.js'e dokunmadan önce seri.test.js'i yaz, kırmızıyı gör, sonra her turda bir davranış ekle.
İpucu 1
Her kural kümesi bir it olsun: "0 ve 1", "2 ve 3", "4", "5 ve üstü". Her birinde sınırın iki yanını dene (1 ile 2, 3 ile 4, 4 ile 5) ve 5'ten çok büyük bir değeri de (12 gibi) ekle.
İpucu 2
İlk kırmızın ERR_MODULE_NOT_FOUND olacak, faizdeki gibi. Sonra return 0 ile ilk testi geçir, ikinci testi ekle…
İpucu 3
Büyükten küçüğe giden if'ler en kısası: önce seri >= 5, sonra seri === 4, sonra seri >= 2, en sonda return 0.
Çözümü göster
import { describe, it } from "node:test"import assert from "node:assert/strict"import { seriAltini } from "./seri.js" describe("seriAltini", () => { it("seri yoksa ya da 1 ise altın yok", () => { assert.strictEqual(seriAltini(0), 0) assert.strictEqual(seriAltini(1), 0) }) it("2 ve 3'lük seri 1 altın", () => { assert.strictEqual(seriAltini(2), 1) assert.strictEqual(seriAltini(3), 1) }) it("4'lük seri 2 altın", () => { assert.strictEqual(seriAltini(4), 2) }) it("5 ve üstü 3 altın", () => { assert.strictEqual(seriAltini(5), 3) assert.strictEqual(seriAltini(12), 3) })})export function seriAltini(seri) { if (seri >= 5) { return 3 } if (seri === 4) { return 2 } if (seri >= 2) { return 1 } return 0}PS C:\kod\oyun-hesap> node --test seri.test.js▶ seriAltini ✔ seri yoksa ya da 1 ise altın yok (0.6365ms) ✔ 2 ve 3'lük seri 1 altın (0.1373ms) ✔ 4'lük seri 2 altın (0.0992ms) ✔ 5 ve üstü 3 altın (0.1832ms)✔ seriAltini (2.7011ms)ℹ tests 4ℹ suites 1ℹ pass 4ℹ fail 0ℹ cancelled 0ℹ skipped 0ℹ todo 0ℹ duration_ms 75.1132Testin işe yaradığını görmek için seri >= 5'i seri > 5 yap: seriAltini(5) 1 döner ve dördüncü test kalır. seri >= 2'yi seri > 2 yaparsan ikinci test kalır. Her sınırın bir bekçisi var.
Envanter özeti
envanter.json adında bir dosya aç:
[ { "esya": "Uzun Kılıç", "fiyat": 350, "adet": 2 }, { "esya": "Can İksiri", "fiyat": 50, "adet": 3 }]Bir de içinde yalnızca [] yazan bos-envanter.json. envanter.js'e, dosya adını alıp { cesit, toplam } döndüren asenkron bir envanterOzeti fonksiyonu yaz: cesit kaç farklı eşya olduğu, toplam her eşyanın fiyatı ile adedinin çarpımlarının toplamı. Sonra envanter.test.js'te üç durumu test et: örnek dosya { cesit: 2, toplam: 850 }, boş dosya { cesit: 0, toplam: 0 }, olmayan dosya ENOENT hatası.
İpucu 1
Dosyayı okumak maclariOku'daki gibi: readFile + JSON.parse. Toplam için Ders 11'deki reduce.
İpucu 2
Dönen değer obje, yani deepEqual. Fonksiyon asenkron, yani test fonksiyonu async ve çağrının önünde await. assert.rejects'in önünde de await olmalı.
Çözümü göster
import { readFile } from "node:fs/promises" export async function envanterOzeti(dosyaAdi) { const metin = await readFile(dosyaAdi, "utf8") const esyalar = JSON.parse(metin) const toplam = esyalar.reduce( (birikim, esya) => birikim + esya.fiyat * esya.adet, 0 ) return { cesit: esyalar.length, toplam }}import { describe, it } from "node:test"import assert from "node:assert/strict"import { envanterOzeti } from "./envanter.js" describe("envanterOzeti", () => { it("çeşit sayısını ve toplam değeri verir", async () => { const ozet = await envanterOzeti("envanter.json") assert.deepEqual(ozet, { cesit: 2, toplam: 850 }) }) it("boş envanterde ikisi de 0", async () => { const ozet = await envanterOzeti("bos-envanter.json") assert.deepEqual(ozet, { cesit: 0, toplam: 0 }) }) it("dosya yoksa hata verir", async () => { await assert.rejects(envanterOzeti("yok.json"), { code: "ENOENT" }) })})PS C:\kod\oyun-hesap> node --test envanter.test.js▶ envanterOzeti ✔ çeşit sayısını ve toplam değeri verir (3.8207ms) ✔ boş envanterde ikisi de 0 (1.0371ms) ✔ dosya yoksa hata verir (1.138ms)✔ envanterOzeti (7.3764ms)ℹ tests 3ℹ suites 1ℹ pass 3ℹ fail 0ℹ cancelled 0ℹ skipped 0ℹ todo 0ℹ duration_ms 74.1101Boş dosya testi reduce'un ikinci parametresini (0) denetliyor: onu unutursan boş dizide reduce hata fırlatır. reduce satırını tek satıra yazdıysan Prettier onu bu üç satıra böler, çünkü 80 karakteri geçiyor; npm run format bunu senin yerine yapar.
Push'tan önce tek komut
GitHub'daki iş akışı üç denetim çalıştırıyor: lint, biçim, test. Aynısını bilgisayarında tek komutla yapmak için package.json'a kontrol adında bir betik ekle; üçünü sırayla çalıştırsın ve biri kalırsa dursun. Sonra iki kez bilerek boz ve her seferinde npm run kontrol'ün hangi adımda durduğuna bak:
can.js'tekireturnsatırının sonuna bir noktalı virgül koy. Durunca dosyayı Prettier'a düzelttir.return'ün hemen üstüneconst kalan = hp - hasarsatırını ekle, amakalan'ı hiçbir yerde kullanma. Mesajı okuyunca satırı sil.
İpucu 1
Komutları && ile bağla: soldaki başarılı biterse sağdaki çalışır. Betik bir npm betiğini çağırabilir: npm run lint && ….
İpucu 2
npm pkg set scripts.kontrol="npm run lint && npm run format:check && npm test". Noktalı virgüllü dosyayı düzeltmek için npm run format.
Çözümü göster
PS C:\kod\oyun-hesap> npm pkg set scripts.kontrol="npm run lint && npm run format:check && npm test"PS C:\kod\oyun-hesap> npm run kontrol > oyun-hesap@1.0.0 kontrol> npm run lint && npm run format:check && npm test > oyun-hesap@1.0.0 lint> eslint . > oyun-hesap@1.0.0 format:check> prettier . --check Checking formatting...[warn] can.js[warn] Code style issues found in the above file. Run Prettier with --write to fix.ESLint geçti (noktalı virgül bir kural ihlali değil), Prettier kaldı ve npm test hiç çalışmadı: && zinciri ilk hatada durur, tıpkı iş akışındaki adımlar gibi. npm run format ile düzelt:
PS C:\kod\oyun-hesap> npm run format > oyun-hesap@1.0.0 format> prettier . --write .github/workflows/test.yml 11ms (unchanged).prettierrc 15ms (unchanged)altin.js 8ms (unchanged)altin.test.js 8ms (unchanged)bos-envanter.json 1ms (unchanged)can.js 1mscan.test.js 4ms (unchanged)...İkinci bozma, kullanılmayan değişken:
PS C:\kod\oyun-hesap> npm run kontrol > oyun-hesap@1.0.0 kontrol> npm run lint && npm run format:check && npm test > oyun-hesap@1.0.0 lint> eslint . C:\kod\oyun-hesap\can.js 5:11 error 'kalan' is assigned a value but never used no-unused-vars ✖ 1 problem (1 error, 0 warnings)Bu kez zincir ilk halkada, ESLint'te koptu; biçim denetimi ve testler hiç çalışmadı. Oysa testler bu dosyada geçerdi: kullanılmayan bir değişken sonucu değiştirmez. ESLint'in yakaladığı şey yanlış sonuç değil, "burada bir şey unutulmuş olabilir" kokusu. Satırı sil ve yeniden çalıştır:
PS C:\kod\oyun-hesap> npm run kontrol > oyun-hesap@1.0.0 kontrol> npm run lint && npm run format:check && npm test...Checking formatting...All matched files use Prettier code style! > oyun-hesap@1.0.0 test> node --test...ℹ tests 29ℹ suites 5ℹ pass 29ℹ fail 0...29 test: anlatımdaki 14 ve görevlerdeki 15. Görevlere farklı sayıda test yazdıysan bu sayı da farklı olur. Push'tan önce npm run kontrol yazmayı alışkanlık edinirsen GitHub'da kırmızı görmen nadirleşir. İş akışında üç adımı ayrı bırakmak yine de daha iyi: Actions sayfasında hangisinin kaldığını adımın adından görürsün.
Dükkân bileşeninin testi
tft-kurucu'da src/Dukkan.test.tsx aç ve Dukkan bileşeni için üç test yaz:
- Arama boşken sekiz birimin sekizi de listelenir (sekiz Ekle düğmesi).
- Kutuya
xyzyazıp Enter'a basınca "Bu adla birim yok." yazısı çıkar veonEklehiç çağrılmaz. - Kutuya
luyazıp Enter'a basıncaonEkle,{ ad: "Lux", maliyet: 3 }ile çağrılır ve kutu boşalır.
Testler geçince tft-kurucu için de .github/workflows/test.yml yaz: npm ci, npm run lint, npm test ve npm run build adımlarıyla.
İpucu 1
Kutuyu yer tutucu yazısıyla bul: screen.getByPlaceholderText("Birim ara, Enter ile ekle"). Birden çok düğme için getAllByRole; dönen dizinin uzunluğunu toHaveLength(8) ile denetle.
İpucu 2
await userEvent.type(kutu, "xyz{Enter}") yazar ve Enter'a basar; süslü parantez içindeki Enter bir tuşun adı. "Hiç çağrılmadı" için expect(onEkle).not.toHaveBeenCalled(), kutunun boşaldığı için expect(kutu).toHaveProperty("value", "").
İpucu 3
İş akışı oyun-hesap'takinin aynısı; yalnız son adımlar değişiyor. tft-kurucu'da lint betiği Oxlint'i çalıştırıyor, build de tsc ile tip denetimi yapıp siteyi derliyor. Biçim denetimi yok, çünkü bu projeye Prettier kurmadın.
Çözümü göster
import { describe, it, expect, vi } from "vitest"import { render, screen } from "@testing-library/react"import userEvent from "@testing-library/user-event"import Dukkan from "./Dukkan.tsx" describe("Dukkan", () => { it("boş aramada bütün birimleri listeler", () => { render(<Dukkan onEkle={vi.fn()} />) expect(screen.getAllByRole("button", { name: "Ekle" })).toHaveLength(8) }) it("olmayan birimde uyarır ve bir şey eklemez", async () => { const onEkle = vi.fn() render(<Dukkan onEkle={onEkle} />) const kutu = screen.getByPlaceholderText("Birim ara, Enter ile ekle") await userEvent.type(kutu, "xyz{Enter}") expect(screen.getByText("Bu adla birim yok.")).toBeTruthy() expect(onEkle).not.toHaveBeenCalled() }) it("Enter ilk eşleşeni ekler ve kutuyu boşaltır", async () => { const onEkle = vi.fn() render(<Dukkan onEkle={onEkle} />) const kutu = screen.getByPlaceholderText("Birim ara, Enter ile ekle") await userEvent.type(kutu, "lu{Enter}") expect(onEkle).toHaveBeenCalledWith({ ad: "Lux", maliyet: 3 }) expect(kutu).toHaveProperty("value", "") })})PS C:\kod\tft-kurucu> npm test > tft-kurucu@0.0.0 test> vitest run RUN v5.0.3 C:/kod/tft-kurucu ✓ src/birimler.test.ts (2 tests) 3ms ✓ src/TakimPaneli.test.tsx (3 tests) 200ms ✓ src/Dukkan.test.tsx (3 tests) 387ms Test Files 3 passed (3) Tests 8 passed (8) Start at 04:03:20 Duration 9.64s (environment 83%, import 13%, tests 2%, transform 1%)lu yalnızca Lux'la eşleşiyor; a yazsaydın Garen, Annie, Yasuo, Ahri ve Aurelion Sol eşleşirdi ve ilk sıradaki Garen eklenirdi. Üçüncü testin işe yaradığını görmek için Dukkan.tsx'te setArama("") satırının başına // koy: test, kutuda hâlâ lu olduğunu söyleyerek kalır.
name: Testler on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - uses: actions/setup-node@v7 with: node-version: 24 - run: npm ci - run: npm run lint - run: npm test - run: npm run buildBu dört adımı bilgisayarında da sırayla çalıştırıp hepsinin hatasız bittiğini görebilirsin; GitHub aynısını Ubuntu'da yapacak.
Kendini sına
Soru 1
Bu test geçer mi?
import { test } from "node:test"import assert from "node:assert/strict"import { faiz } from "./faiz.js" test("eksi altında faiz 0", () => { assert.strictEqual(faiz(-5), 0)})Cevabı göster
Kalır: -1 !== 0. Math.floor(-5 / 10) sonucu -0.5'in aşağı yuvarlanmışı, yani -1; Math.min(5, -1) de -1. TDD turlarında eksi altını hiç düşünmedik, o yüzden bu sınır açık kaldı. Bu test yeni bir turun kırmızısı olabilir: düzeltmesi Math.max(0, …) ile alt sınır koymak.
Soru 2
Neden kaldı?
assert.strictEqual(altinPaylas(1234, 5), { kisiBasi: 246, artan: 4 })Cevabı göster
strictEqual, objeleri === ile karşılaştırır; iki ayrı obje içerikleri aynı olsa da === değildir. Mesaj Values have same structure but are not reference-equal olur. Obje ve dizilerde assert.deepEqual (Vitest'te toEqual) kullanılır.
Soru 3
Rapor tamamen yeşil ama bir şey eksik
Yeni bir projedeki tek test dosyan kdaTesti.js ve içinde beş test var. npm test (yani node --test) şunu yazıyor: ℹ tests 0, ℹ pass 0, ℹ fail 0. Sorun ne?
Cevabı göster
Dosya adı node --test'in aradığı kalıplara uymuyor, o yüzden hiç çalıştırılmadı. fail 0 yazması "hata yok" değil, "hiçbir şey denetlenmedi" demek. Dosyanın adını kda.test.js gibi .test.js ile biten bir ada çevir.
Soru 4
Hangi araç yakalar?
Aşağıdaki üç sorunu hangisi yakalar: test, ESLint, Prettier?
if (hp = 0)yazılmış.- Bir dosyada girintiler 2, ötekinde 4 boşluk.
kalanCan(100, 250)sonucu-150.
Cevabı göster
- ESLint (
no-cond-assign). Node hiç uyarmaz; koşul sessizce hep yanlış olur. - Prettier. Anlamı etkilemez, yalnızca görünüş;
prettier --checklisteler,--writedüzeltir. - Test. Kod kurallara uygun ve düzgün biçimli; yalnızca sonucu yanlış. Bunu ancak "100 cana 250 hasar 0 bırakmalı" diyen bir test söyleyebilir.
Soru 5
Test ✔ ama dosya ✖
Bir test raporunda testin kendi satırı ✔ olmayan dosya hata verir, hemen altında generated asynchronous activity after the test ended diye başlayan bir satır var. Testte büyük ihtimalle ne unutuldu?
Cevabı göster
assert.rejects(...)'in önündeki await. Test fonksiyonu Promise'in sonucunu beklemeden bitti, bu yüzden test "geçti" göründü; denetimin sonucu test kapandıktan sonra geldi. Promise döndüren her denetimin önüne await yaz.
Soru 6
Neden önce kırmızı?
TDD'de yeni bir testi yazdıktan sonra, kodu yazmadan önce neden çalıştırıp kaldığını görmek istiyoruz?
Cevabı göster
Hiç kalmayan bir test bir şey kanıtlamaz. Yanlış fonksiyonu import ediyor, hiçbir assert içermiyor ya da her zaman doğru olan bir şeyi denetliyor olabilir. Testi önce kırmızı görmek, onun gerçekten o davranışa baktığını ve davranış eksikken kalabildiğini gösterir.
Soru 7
CI'da neden npm install değil de npm ci?
Cevabı göster
npm ci yalnızca package-lock.json'da yazan sürümleri kurar, kilit dosyasını değiştirmez ve dosya package.json ile uyuşmazsa durur. Böylece GitHub'daki geçici bilgisayar senin bilgisayarındaki sürümlerin aynısıyla test eder. npm install ise kilit dosyasını güncelleyebilir ve "bende çalışıyordu" türü farklara yol açar.
Kayıt noktası
Bir sonraki derste bütün bu projeleri bir portföye dönüştüreceksin: GitHub profilin, README'lerin, CV'n ve iş başvuruların.