İçeriğe geç
XDD.baby Yazılım Öğrenme
Sözlük Hata rehberi 0/56

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:test ve node:assert/strict ile test dosyası yazıp node --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
  1. Neden test: güvenle değiştirmek
  2. Dersin projesi: oyun-hesap
  3. assert: beklediğini yaz, Node denetlesin
  4. node:test: testlere ad vermek
  5. Birim testi ve sınır durumları
  6. Önce test (TDD) denemesi
  7. Asenkron fonksiyonları test etmek
  8. React bileşen testine ilk bakış
  9. ESLint ve Prettier: kod kalitesinin iki bekçisi
  10. GitHub Actions: her push'ta test
  11. Görevler
  12. Kendini sına
  13. 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.

Kodu değiştir kısalt, düzelt, ekle npm test bütün testler, 1 sn ✔ Hepsi geçti güvenle commit et ✖ Biri kaldı hangi test, ne bekledi düzelt, yeniden çalıştır
Şekil 53.1 Testlerin varsa kodu değiştirmek bir döngüye dönüşür: değiştir, çalıştır, sonuca bak.

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:

PowerShell
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:

kda.js
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:

altin.js
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ç:

deneme.js
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")
Terminal — PowerShell
PS C:\kod\oyun-hesap> node deneme.jsİki denetim de geçti

assert.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.

Sık hataAssertionError [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.

deneme.js
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")
Hata çıktısı
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.0

Nası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:

nesne.js
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")
Hata çıktısı
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, undefined karşı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.

kda.test.js — oyun-hesap — Visual Studio Code
kda.test.js
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")})
PROBLEMSOUTPUTDEBUG CONSOLETERMINAL
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.3322
Ln 1, Col 1Spaces: 4UTF-8JavaScript

Her 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.

test("ölüm varsa KDA iki basamakla yazılır", () => { assert.strictEqual(kdaMetni(7, 2, 9), "8.00") }) test yeni bir test kaydı aç "…" testin adı: raporda bu cümle görünür assert.strictEqual iki değeri === ile karşılaştır kdaMetni(7, 2, 9) gerçek değer: fonksiyonun döndürdüğü "8.00" beklenen değer: senin yazdığın doğru cevap
Şekil 53.2 Bir testin parçaları. Ad, raporda okuyacağın cümledir; o yüzden "ne olmalı"yı anlatsın.

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.

Terminal — PowerShell
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:

Terminal — PowerShell
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.1739

npm 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:

kda.js
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:

Terminal — PowerShell
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:

✖ ölüm varsa KDA iki… 1 test at kda.test.js:5:1 2 AssertionError 3 '8' !== '8.00' 4 at …kda.test.js:6:12 5 ℹ fail 1 6 Kalan testin adı Testin başladığı satır Bir assert tutmadı Solda gelen, sağda beklenen Tutmayan assert'in satırı Kaç test kaldı
Şekil 53.3 Kalan bir testin raporu yukarıdan aşağı okunur. ... 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.

Sık hataℹ 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:

Terminal — PowerShell
PS C:\kod\oyun-hesap> node --testℹ tests 0ℹ suites 0ℹ pass 0ℹ fail 0ℹ cancelled 0ℹ skipped 0ℹ todo 0ℹ duration_ms 10.5027

Tehlikeli 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:

altin.test.js
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 })    })})
Terminal — PowerShell
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.2627

describe 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:

altin.js
export function altinPaylas(toplam, kisiSayisi) {    return {        kisiBasi: Math.round(toplam / kisiSayisi),        artan: toplam % kisiSayisi    }}
Terminal — PowerShell
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:

SorualtinPaylas 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:

dene.js
import { altinPaylas } from "./altin.js" console.log(altinPaylas(100, 0))
Terminal — PowerShell
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:

altin.test.js
    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:

Terminal — PowerShell
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:

altin.js
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 }}
Terminal — PowerShell
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.0806

Aynı yolla ısınmadaki 0/0/0 sorununu kapat. Hiç katkısı olmayan oyuncu "Kusursuz" değil 0.00 almalı. kda.test.js'in sonuna:

kda.test.js
test("hiç katkısı olmayan oyuncu 0.00 alır", () => {    assert.strictEqual(kdaMetni(0, 0, 0), "0.00")})
Terminal — PowerShell
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:

kda.js
export function kdaMetni(kill, olum, asist) {    if (kill + asist === 0) {        return "0.00"    }    if (olum === 0) {        return "Kusursuz"    }    return ((kill + asist) / olum).toFixed(2)}
Terminal — PowerShell
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.842

Dokuzu 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.

1. Kırmızı testi yaz, kalsın 2. Yeşil geçirecek en az kod 3. Düzenle testler yeşilken güzelleştir kodu yaz, yeşili gör sıradaki davranışa geç
Şekil 53.4 TDD'nin üç adımı bir döngüdür. Her turda tek bir yeni davranış eklenir.

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:

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:

Terminal — PowerShell
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ı:

faiz.js
export function faiz(altin) {    return 0}
Terminal — PowerShell
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:

faiz.test.js
test("her 10 altın için 1 faiz", () => {    assert.strictEqual(faiz(10), 1)    assert.strictEqual(faiz(25), 2)    assert.strictEqual(faiz(49), 4)})
Terminal — PowerShell
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:

faiz.js
export function faiz(altin) {    return Math.floor(altin / 10)}
Terminal — PowerShell
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:

faiz.test.js
test("faiz en fazla 5 olur", () => {    assert.strictEqual(faiz(50), 5)    assert.strictEqual(faiz(80), 5)})
Terminal — PowerShell
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:

faiz.js
export function faiz(altin) {    return Math.min(5, Math.floor(altin / 10))}
Terminal — PowerShell
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:

faiz.js
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:

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:

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:

maclar.test.js
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" })})
Terminal — PowerShell
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:

Sık hataundefined !== 3: testte await unutmak

Ne zaman çıkar? Asenkron fonksiyonun sonucunu await yazmadan kutuya koyup içine bakınca.

maclar.test.js
test("örnek dosyada üç maç var", async () => {    const maclar = maclariOku("ornek-maclar.json")    assert.strictEqual(maclar.length, 3)    assert.strictEqual(maclar[0].sampiyon, "Ahri")})
Terminal — PowerShell
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:

maclar.js
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:

Sık hatagenerated asynchronous activity after the test ended

Ne 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.

maclar.test.js
test("olmayan dosya hata verir", async () => {    assert.rejects(maclariOku("yok.json"), { code: "ENOENT" })})
Terminal — PowerShell
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; .tsx dosyaları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ığı):

Terminal — PowerShell
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 vulnerabilities

Bu 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ç:

src/birimler.test.ts
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.

Terminal — PowerShell
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:

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'deki App nası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:

Sık hataReferenceError: document is not defined

Ne 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.

Terminal — PowerShell
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:

vite.config.ts
/// <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 dosyada test diye bir bölüm olabilir" der. Yazmazsan npm run build sırasında tsc bu bölüme 'test' does not exist in type 'UserConfigExport' diye itiraz eder.
  • environment: 'jsdom': testleri jsdom'un sahte sayfasında çalıştır; document artık var.
  • globals: true: Vitest'in describe, it, afterEach gibi adlarını her yerden erişilebilir yapar. Testing Library her testten sonra çizdiği bileşeni sayfadan silmek için genel bir afterEach arar; bulamazsa silmez. Bu satırı unutursan ne olacağı hemen aşağıdaki kartta.

Son olarak test betiğini ayarla ve çalıştır:

Terminal — PowerShell
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.

Sık hataTestingLibraryElementError: 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.

Terminal — PowerShell
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):

.gitignore
node_modules

Sonra ESLint'i kur. Bu ders ESLint 10.12.0 ile denendi:

Terminal — PowerShell
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 vulnerabilities

ESLint'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:

eslint.config.js
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 .js dosyaları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ı olan console, process gibi adları tanıt. globals paketi 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:

rapor.js
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)
Terminal — PowerShell
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.

KuralNe diyorDüzeltme
no-unused-varsfaiz alındı ama hiç kullanılmadıSatırı sil
no-cond-assignif içinde karşılaştırma yerine atama (=) var=== yaz (Ders 2)
no-constant-conditionmac.olum = 0 her zaman 0, yani koşul hep yanlışAynı düzeltme
no-undefmacSayisi diye bir kutu yokmaclar.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:

rapor.js
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)
Terminal — PowerShell
PS C:\kod\oyun-hesap> npx eslint .PS C:\kod\oyun-hesap> node rapor.jsAhri: NaNJinx hiç ölmediJinx: KusursuzToplam maç: 2

ESLint 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:

Terminal — PowerShell
PS C:\kod\oyun-hesap> node rapor.jsAhri: 8.00Jinx hiç ölmediJinx: KusursuzToplam maç: 2

Bu küçük olay, araçların neden birlikte kullanıldığını gösteriyor:

AraçNeyi yakalarBu dosyada
TestlerYanlış sonuçrapor.js için test olsaydı NaN'ı yakalardı
ESLintŞüpheli kalıp, tanımsız ad= yerine ===, macSayisi, kullanılmayan import
TypeScriptYanlış tip, olmayan alanmac.asit'i yazarken altını çizerdi (Ders 39)
PrettierDağınık biçimBirazdan
Sık hata'console' is not defined no-undef

Ne 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:

Terminal — PowerShell
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:

Terminal — PowerShell
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 vulnerabilities

Ayarları .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:

.prettierrc
{    "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'teki LF will be replaced by CRLF uyarı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:

.prettierignore
package.jsonpackage-lock.json

Bu 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:

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:

Terminal — PowerShell
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:

seviye.js
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:

Terminal — PowerShell
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:

package.json
  "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ğer bir 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ı:

.github/workflows/test.yml — oyun-hesap — Visual Studio Code
.github/workflows/test.yml
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 test
Ln 1, Col 1Spaces: 4UTF-8Metin

Sitede 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. @v7 kullanılan sürüm.
  • actions/setup-node@v7: Node kurar. with: ona ayar verir; node-version: 24. with'in uses ile 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.

Sık hatanpm 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:

Terminal — PowerShell
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ı:

Terminal — PowerShell
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.js

Listede 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:

PowerShell
git remote add origin https://github.com/denizyilmaz/oyun-hesap.gitgit branch -M maingit push -u origin main

Push'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ç:

  1. Deponun adının altındaki sekmelerden Actions'a tıkla. Solda iş akışlarının listesi var; Testler orada.
  2. 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.
  3. 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 tests raporunun 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.

Senin bilgisayarın git push GitHub deposu test.yml'ı okur Geçici bilgisayar (ubuntu-latest) checkout setup-node npm ci lint format:check test ✔ hepsi geçti: commit'te yeşil tik ✖ ilk kalan adımda durur
Şekil 53.5 Bir push'tan sonra olanlar. Senin bilgisayarın yalnızca kodu gönderir; gerisi GitHub'ın açtığı geçici bilgisayarda olur.

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.

Görev 1

Can hiç eksiye düşmesin

can.js adında bir dosyaya şu fonksiyonu yaz:

can.js
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
can.test.js
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ı:

Terminal — PowerShell
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:

can.js
export function kalanCan(hp, hasar) {    if (hasar < 0) {        throw new Error("Hasar eksi olamaz")    }    return Math.max(0, hp - hasar)}
Terminal — PowerShell
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.6723

Math.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.

Görev 2

Lux'un son yeteneği

mana.js'teki fonksiyonun bir hatası var:

mana.js
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
mana.test.js
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)    })})
Terminal — PowerShell
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:

mana.js
export function yetenekHazirMi(mana, bedel) {    return mana >= bedel}
Terminal — PowerShell
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ı.

Görev 3

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
seri.test.js
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)    })})
seri.js
export function seriAltini(seri) {    if (seri >= 5) {        return 3    }    if (seri === 4) {        return 2    }    if (seri >= 2) {        return 1    }    return 0}
Terminal — PowerShell
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.1132

Testin 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.

Görev 4

Envanter özeti

envanter.json adında bir dosya aç:

envanter.json
[    { "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
envanter.js
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 }}
envanter.test.js
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" })    })})
Terminal — PowerShell
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.1101

Boş 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.

Görev 5

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:

  1. can.js'teki return satırının sonuna bir noktalı virgül koy. Durunca dosyayı Prettier'a düzelttir.
  2. return'ün hemen üstüne const kalan = hp - hasar satırını ekle, ama kalan'ı 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
Terminal — PowerShell
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:

Terminal — PowerShell
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:

Terminal — PowerShell
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:

Terminal — PowerShell
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.

Görev 6

Dükkân bileşeninin testi

tft-kurucu'da src/Dukkan.test.tsx aç ve Dukkan bileşeni için üç test yaz:

  1. Arama boşken sekiz birimin sekizi de listelenir (sekiz Ekle düğmesi).
  2. Kutuya xyz yazıp Enter'a basınca "Bu adla birim yok." yazısı çıkar ve onEkle hiç çağrılmaz.
  3. Kutuya lu yazıp Enter'a basınca onEkle, { 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
src/Dukkan.test.tsx
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", "")    })})
Terminal — PowerShell
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.

.github/workflows/test.yml
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 build

Bu 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?

JavaScript
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ı?

JavaScript
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?

  1. if (hp = 0) yazılmış.
  2. Bir dosyada girintiler 2, ötekinde 4 boşluk.
  3. kalanCan(100, 250) sonucu -150.
Cevabı göster
  1. ESLint (no-cond-assign). Node hiç uyarmaz; koşul sessizce hep yanlış olur.
  2. Prettier. Anlamı etkilemez, yalnızca görünüş; prettier --check listeler, --write düzeltir.
  3. 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.

İşaret yalnızca bu tarayıcıda saklanır, kimseye gönderilmez.