Kata'larla TDD Pratiği: String Calculator, Bowling Game ve Bilinçli Tekrar
Kata, cevabı bilinen küçük bir problemi defalarca çözerek TDD ritmini bilinçli tekrarla geliştirmektir; başarı ölçütü bitirmek değil adım kalitesidir. Coding dojo, randori ve ping-pong formatlarını, String Calculator'ı C# ve xUnit v3 Theory ile, Bowling Game'i TypeScript ve Vitest ile çözerken test list'in evrimini, kritik karar anlarını ve kata'yı zorlaştıran varyasyonları gerçek çıktılarla görüyoruz.
Sürüm notu: Bu yazıdaki kodlar .NET 10 ve xUnit.net v3 4.0.3 ile, TypeScript örnekleri Node.js 24 ve Vitest 5.1.0 ile derlenip denenmiştir.
Önce Tahmin Et
String Calculator kata'sının ilk adımında bir Add(string) method'u yazıyorsun: boş string 0, "1" 1, "1,2" 3 döner. Üçüncü testi geçirmek için şu satırı yazdın:
public static int Add(string numbers) =>
numbers.Length == 0 ? 0 : numbers.Split(',').Sum(int.Parse);
Kata'nın ikinci adımı diyor ki: method bilinmeyen sayıda sayıyı toplayabilsin. Bunun için [InlineData("1,2,3,4", 10)] ekliyorsun. Test kırmızı mı olur, yeşil mi? Yeşil olursa bu iyi bir işaret mi, kötü bir işaret mi? Bir cevap seç; kata boyunca birkaç kez bu soruya döneceğiz.
Kata Nedir ve Neden Aynı Problemi Tekrar Çözeriz?
"Code kata" terimini 2000'lerin başında The Pragmatic Programmer'ın yazarlarından Dave Thomas yaygınlaştırdı. Kelime dövüş sanatlarından geliyor: kata, bir ustanın öğrencilerine tekrar tekrar yaptırdığı, hareketleri belli bir dizi. Amaç dövüşü kazanmak değil, hareketi o kadar çok tekrarlamak ki gerçek bir an geldiğinde düşünmeden doğru yapmak.
Yazılımdaki karşılığı şu: küçük, sınırları belli, cevabı zaten bilinen bir problem seçersin ve onu defalarca çözersin. Cevabı bilmek burada bir sorun değil, tam tersine şart. Problem seni yormadığında dikkatini sürece verebilirsin: test list'i nasıl kurdun, hangi testi önce seçtin, adımların ne kadar büyüktü, refactor'ı nerede yaptın, nerede atladın. TDD (Test-Driven Development; önce testi, sonra kodu yazma yaklaşımı) bir beceri ve beceriler production kodunun baskısı altında değil, güvenli bir ortamda bilinçli tekrarla gelişir.
Bu yüzden bir kata oturumunun başarı ölçütü kata'yı bitirmek değildir. Otuz dakikalık bir oturumda String Calculator'ın dokuz adımından dördünü bitirmek, ama her birinde kırmızıyı doğru nedenle görmek, testleri ve kodu her yeşilde temizlemek, dokuz adımı bir saatte tek bir dev commit ile bitirmekten çok daha değerlidir. Roy Osherove'un String Calculator sayfası da aynı yöne işaret eder: kata'yı her gün kısa bir süre yap, ileriyi okuma, adımları birer birer al.
Zaman kutusu bunun aracı. Code retreat gibi formatlarda her oturumun sonunda kod silinir: amaç kodu biriktirmek değil, bir sonraki seferde aynı yolu daha iyi yürümek. Silmekten çekiniyorsan, bu kata'yı production kodu gibi görmeye başladığının işaretidir.
Coding Dojo, Randori ve Pair Formatları
Kata tek başına da yapılır, ama asıl öğrenme başkalarının adımlarını izlerken gelir. Coding dojo, bu iş için bir araya gelen grubun adı. Codingdojo.org'daki tarihçe, fikrin Dave Thomas'ın kata'larından Laurent Bossavit'in bir dojo çağrısına ve Emmanuel Gaillot ile başlayan Paris dojo'suna uzandığını anlatır. Paris'teki oturumlar bir, bir buçuk saat sürüyordu ve genelde boş bir dosyadan başlıyordu.
İki yaygın format var. Prepared kata'da bir kişi çözümü önceden hazırlar ve grubun önünde, TDD ile küçük adımlarla, her adımı anlatarak baştan yazar. İzleyenler yalnız anlamadıkları bir yer olduğunda araya girer. Yeni bir tekniği ya da bir dili göstermek için iyidir. Randori'de ise hazırlık yoktur: grup bir kata seçer, iki kişi klavyeye geçer, düşündüklerini sesli anlatarak yazarlar, izleyenler özellikle testler yeşilken öneri yapar. Sürücü belli bir kurala göre değişir.
Değişim kurallarından en kullanışlı ikisi şunlar. Time box: her ikili 5–7 dakika klavyede kalır, süre bitince sürücü izleyicilere döner, yardımcı pilot sürücü olur, izleyicilerden biri yeni yardımcı pilot olur. Ping-pong: biri kırmızı testi yazar, diğeri geçirir, refactor eder ve bir sonraki kırmızı testi yazıp klavyeyi geri verir. Ping-pong, testi yazanla kodu yazanı ayırdığı için "testi geçirmek için en basit şey" fikrini çok iyi öğretir: karşındakinin testini fake ile geçirmek, onu daha iyi bir test yazmaya zorlar.
String Calculator: Test List ve İlk Adımlar
String Calculator, Roy Osherove'un TDD kata'larının ilki. Kata, bir string'deki sayıları toplayan bir method'u adım adım büyütür: önce virgülle ayrılmış birkaç sayı, sonra yeni satır ayırıcısı, sonra ilk satırda tanımlanan özel ayırıcı, sonra negatif sayılar için exception ve daha ileri adımlar. Kendi kurgumuzla, C# ve xUnit v3 ile ilk altı adımı çözüyoruz. Test list'imiz:
// Test list: StringCalculator.Add
// [ ] Boş string: 0
// [ ] Tek sayı: kendisi
// [ ] İki sayı, virgülle: toplamı
// [ ] Bilinmeyen sayıda sayı
// [ ] Yeni satır da ayırıcıdır: "1\n2,3" -> 6
// [ ] İlk satırda özel ayırıcı: "//;\n1;2" -> 3
// [ ] Negatif sayılar exception fırlatır, mesajda hepsi listelenir
İlk iki madde çok küçük adımlar. Boş string için return 0; yazıp yeşile geçiyoruz (fake it). Tek sayı için int.Parse ekliyoruz. Aynı davranışın farklı örnekleri olduğu için bunları bir [Theory] içinde topluyoruz; veri odaklı testlerin ayrıntısı Unit Test serisinin Theory yazısında. Üçüncü veri satırını eklediğimizde kırmızı, assertion'dan değil exception'dan geliyor:
failed Kata.Tests.StringCalculatorTests.Add_returns_the_sum_of_the_numbers(numbers: "1,2", expected: 3) (5ms)
Xunit.MicrosoftTestingPlatform.XunitException: System.FormatException : The input string '1,2' was not in a correct format.
at Kata.StringCalculator.Add(String numbers) in src/Kata/StringCalculator.cs:5
…
total: 3
failed: 1
succeeded: 2
Theory'nin her veri satırı ayrı bir test olarak raporlanır ve test adında parametreleri görürsün; hangi örneğin kırıldığını aramana gerek kalmaz. "Önce Tahmin Et"teki Split'li satırla üç test yeşil.
String Calculator: Kritik Karar Anları
Şimdi tahmin sorusunun cevabı. "1,2,3,4" satırını ekleyince test ilk koşuda yeşil. Kata'nın ikinci adımı, biz farkında olmadan birinci adımın içinde çözülmüş: Split ile Sum zaten sayı adedinden bağımsız. Bu iyi bir şey mi? Kısmen. Davranış doğru, ama kata açısından bir adımı atlamış olduk. Üçüncü testte fake it ile parts[0] + parts[1] yazsaydık, ikinci adım bizi gerçek genellemeye zorlayacak ve triangulation'ı yaşayacaktık. Kata'da bu bir hata değil, bir gözlem: bir sonraki tekrarda adımı bilinçli olarak küçültmeyi deneyebilirsin. Kata'yı tekrar etmenin değeri tam olarak bu tür gözlemlerden geliyor.
Yeni satır maddesi tekrar FormatException ile kırmızı. Çözüm Split([',', '\n']). Özel ayırıcı maddesi ise ilk gerçek tasarım kararını getiriyor:
failed Kata.Tests.StringCalculatorTests.Add_returns_the_sum_of_the_numbers(numbers: "//;\n1;2", expected: 3) (0ms)
Xunit.MicrosoftTestingPlatform.XunitException: System.FormatException : The input string '//;' was not in a correct format.
Girdinin artık iki parçası var: isteğe bağlı bir başlık satırı ve sayılar. İlk yeşil, başlığı Add'in içinde işleyen birkaç satırdı. Negatif sayılar maddesine geçince Add iki iş yapmaya başladı: girdiyi ayrıştırmak ve kural uygulamak. Negatif testini yeşile çevirdikten sonra ayrıştırmayı kendi method'una taşıdık:
namespace Kata;
public static class StringCalculator
{
public static int Add(string numbers)
{
if (numbers.Length == 0)
{
return 0;
}
int[] values = Parse(numbers);
int[] negatives = [.. values.Where(v => v < 0)];
if (negatives.Length > 0)
{
throw new ArgumentException($"Negatives not allowed: {string.Join(", ", negatives)}");
}
return values.Sum();
}
private static int[] Parse(string input)
{
char[] delimiters = [',', '\n'];
if (input.StartsWith("//"))
{
delimiters = [.. delimiters, input[2]];
input = input[4..];
}
return [.. input.Split(delimiters).Select(int.Parse)];
}
}
Testlerin son hâli:
public sealed class StringCalculatorTests
{
[Theory]
[InlineData("", 0)]
[InlineData("1", 1)]
[InlineData("1,2", 3)]
[InlineData("1,2,3,4", 10)]
[InlineData("1\n2,3", 6)]
[InlineData("//;\n1;2", 3)]
public void Add_returns_the_sum_of_the_numbers(string numbers, int expected)
{
int sum = StringCalculator.Add(numbers);
Assert.Equal(expected, sum);
}
[Fact]
public void Add_rejects_negatives_and_lists_them_all()
{
ArgumentException ex = Assert.Throws<ArgumentException>(() => StringCalculator.Add("1,-2,3,-4"));
Assert.Equal("Negatives not allowed: -2, -4", ex.Message);
}
}
Test run summary: Passed!
total: 7
failed: 0
succeeded: 7
skipped: 0
duration: 297ms
Negatif testinin ilk kırmızısı da öğretici: Assert.Throws() Failure: No exception was thrown. Mesajda tek negatif değil hepsinin listelenmesi gerektiği için test verisinde bilerek iki negatif var; tek negatifle yazılmış bir test, ilk negatifte exception fırlatan yarım bir implementasyonu da yeşil gösterirdi.
Bowling Game: TypeScript ve Vitest ile
İkinci kata, Robert C. Martin'in (Uncle Bob) uzun yıllardır anlattığı Bowling Game. Problem tek cümle: on frame'lik bir bowling oyununun atışları verildiğinde toplam skoru hesapla. Kural ayrıntıları burada önemli: bir frame'de iki atışla on lobutun hepsini devirmek spare'dir ve bonus olarak sonraki tek atış eklenir; ilk atışta hepsini devirmek strike'tır ve sonraki iki atış bonus olur. Son frame'de bonus atışlar oynanır, mükemmel oyun 300'dür.
Bu kez TypeScript ve Vitest ile çözüyoruz. Kata için bir framework'e gerek yok; boş bir klasörde npm install -D vitest yeterli. Vitest TypeScript dosyasını tip kontrolü yapmadan dönüştürüp çalıştırır, bu da döngüyü hızlı tutar. Angular projelerinde Vitest kurulumu ilerideki Frontend Testleri serisinin konusu.
İlk test oluk oyunu: yirmi atışın hepsi sıfır, skor sıfır. Modül olmadığı için ilk kırmızı Vitest'ten geliyor: Error: Cannot find module './bowling-game'. score()'un 0 döndürdüğü bir class ile yeşile geçiyoruz. İkinci test, yirmi atışın hepsi bir, skor yirmi:
AssertionError: expected +0 to be 20 // Object.is equality
Atışları roll() içinde bir toplama eklemek obvious implementation. İki test yeşil. Refactor adımında testlerdeki döngü tekrarını rollMany yardımcısına topluyoruz.
Bowling Game: Tasarımın Yanlış Olduğunu Anlamak
Kata'nın en meşhur anı üçüncü testte gelir. Spare: 5 ve 5, ardından 3, gerisi sıfır. Skor 10 + 3 bonus + 3 = 16 olmalı.
× adds the next roll as a spare bonus 2ms
AssertionError: expected 13 to be 16 // Object.is equality
Tests 1 failed | 2 passed (3)
Bu kırmızıyı yeşile çevirmek için roll()'a "önceki iki atış onu tuttuysa bu atışı iki kez say" gibi bir if yazmak mümkün. Ama o anda fark edilen şey şu: skor, atışın yapıldığı anda değil, frame'lere bakılarak hesaplanmalı. Mevcut tasarımda roll() hesaplıyor, score() yalnız sonucu döndürüyor. Yanlış yer.
Martin'in slaytlarında bu an, sorumlulukların yanlış yerde olduğu tespitiyle gelir: tasarım yanlış. Böyle bir anda TDD pratiğinde sık kullanılan hamle, kata'nın asıl dersi: yeni testi geçici olarak devre dışı bırak, eski testler yeşilken tasarımı düzelt, sonra testi geri aç. Kırmızı testle refactor yapılmaz; refactor'ın güvencesi yeşil testlerdir.
it.skip('adds the next roll as a spare bonus', () => {
Test Files 1 passed (1)
Tests 2 passed | 1 skipped (3)
Yeşil zeminde roll()'u atışları saklayan, score()'u frame frame dolaşan hâle getiriyoruz. Davranış aynı, iki test yeşil. skip'i kaldırıp spare kuralını frame döngüsüne ekleyince üç test yeşil. Strike testi de bir sürpriz getiriyor:
AssertionError: expected NaN to be 24 // Object.is equality
NaN, frame döngüsünün strike'tan sonra iki atış ilerlemesinden geliyor: dizi sonuna gelince undefined ile toplama yapılıyor. Kırmızı yine eksik davranışı anlatıyor: strike frame'i tek atıştır. Strike dalını ekleyip refactor ettikten sonraki kod:
export class BowlingGame {
private readonly rolls: number[] = [];
roll(pins: number): void {
this.rolls.push(pins);
}
score(): number {
let score = 0;
let frameStart = 0;
for (let frame = 0; frame < 10; frame++) {
if (this.isStrike(frameStart)) {
score += 10 + this.rolls[frameStart + 1] + this.rolls[frameStart + 2];
frameStart += 1;
} else if (this.isSpare(frameStart)) {
score += 10 + this.rolls[frameStart + 2];
frameStart += 2;
} else {
score += this.rolls[frameStart] + this.rolls[frameStart + 1];
frameStart += 2;
}
}
return score;
}
private isStrike(frameStart: number): boolean {
return this.rolls[frameStart] === 10;
}
private isSpare(frameStart: number): boolean {
return this.rolls[frameStart] + this.rolls[frameStart + 1] === 10;
}
}
Son test mükemmel oyun: on iki strike, 300. İlk koşuda yeşil ve bu kez bu beklenen bir şey: test bir davranış eklemiyor, anlayışımızı doğruluyor.
✓ bowling-game.test.ts > BowlingGame > scores a gutter game as 0 1ms
✓ bowling-game.test.ts > BowlingGame > scores all ones as 20 0ms
✓ bowling-game.test.ts > BowlingGame > adds the next roll as a spare bonus 0ms
✓ bowling-game.test.ts > BowlingGame > adds the next two rolls as a strike bonus 0ms
✓ bowling-game.test.ts > BowlingGame > scores a perfect game as 300 0ms
Test Files 1 passed (1)
Tests 5 passed (5)
Beş testin tamamı 65 ms'de koştu. Bu hız, kata'yı TDD çalışmak için iyi bir zemin yapıyor: döngü saniyeler içinde dönüyor ve dikkat hep adımın kendisinde kalıyor.
Kata'yı Zorlaştırmak ve Kataloglar
Bir kata'yı birkaç kez çözdükten sonra çözüm ezberlenir ve kata bir şey öğretmemeye başlar. O noktada kuralları değiştirmek gerekir. Yaygın varyasyonlar:
- Yalnız klavye: fare yasak. IDE'nin refactor kısayollarını öğrenmenin en hızlı yolu; rename, extract method ve inline'ı elle yapmayı bırakırsın.
- if yasak: koşullar polymorphism, lookup tablosu ya da veri yapısıyla ifade edilir. Bowling Game'de frame türlerini ayrı type'lar olarak modellemeye zorlar.
- Primitive yasak: sayılar ve string'ler küçük domain type'larına sarılır. Jeff Bay'in The ThoughtWorks Anthology (2008) kitabındaki Object Calisthenics kurallarından biri; String Calculator'da
DelimiterveNumbersgibi type'lar doğurur. - Farklı başlangıç: bir kez en zor maddeyle başlamak, bir kez en kolayla. Önceki yazıdaki "sıra tasarımı etkiler mi?" sorusunu doğrudan deneyimletir.
- Farklı dil ya da framework: aynı kata'yı tanımadığın bir dilde çözmek, TDD ritmini dilin yeni syntax'ından ayırt etmeyi öğretir.
Codingdojo.org'daki katalog elliden fazla kata listeler: FizzBuzz, Roman Numerals, Tennis, Mars Rover, Game of Life, Word Wrap gibi. Seçim yaparken iki ölçüt işe yarar: problemi on dakikada anlatabilmelisin ve ilk testi beş dakikada yazabilmelisin. Gilded Rose gibi bazı kata'lar ise sıfırdan TDD için değil, mevcut kodu teste almak için tasarlanmıştır; onları ilerideki Refactoring serisine bırakıyoruz.
Üretimde Dikkat
Kata pratiğini ekibe taşımanın işe yarayan şekli küçük ve düzenli bir oturum: haftada bir, 45–60 dakika, kod review ya da sprint toplantısıyla çakışmayan bir saat. Önerilen akış: beş dakika kata'yı ve o haftanın kısıtını seçmek, otuz dakika randori ya da ikili ping-pong, on dakika retrospektif. Retrospektif en değerli kısım: "nerede adımımız büyüdü?", "hangi kırmızı bizi şaşırttı?", "refactor'ı nerede atladık?" soruları, production kodundaki alışkanlıkları da konuşulur hâle getirir.
İlk birkaç oturumda kolaylaştırıcının prepared kata ile başlaması, ekibe ritmi göstermek için iyi. Sonra randori'ye geçilir. Kod saklanmaz; oturum sonunda silinir. Saklamak isteyenler varsa kendi branch'lerinde tutsunlar, ama bir sonraki oturum yine boş dosyadan başlar.
Kata'nın faydasını ölçmek zor ve ölçmeye çalışmak da onu bozabilir: kata sayısı ya da "bitirilen adım" metriği, insanları ezbere koşmaya iter. Daha iyi sinyaller dolaylıdır. Pull request'lerde test ile kodun aynı commit'lerde, küçük adımlarla geldiğini görmeye başlarsın; review'da "bu testi önce mi yazdın?" sorusu doğal bir soru olur; refactor commit'leri davranış commit'lerinden ayrılır.
Kata'da öğrenilen ritim production koduna olduğu gibi taşınmaz. Kata'da bağımlılık, veritabanı ya da framework yoktur; gerçek kodda vardır. O yüzden kata'nın asıl kazancı tekniğin kendisi değil, adım boyunu sezme ve kırmızıyı okuma becerisidir. Bağımlılıkların olduğu yerde TDD'nin nasıl göründüğünü bir sonraki yazıda, dıştan içe ilerleyerek göreceğiz.
Sık Düşülen Tuzaklar
Kata çözümünü ezberleyip doğru cevaba koşmak. Bowling Game'in son hâlini bildiğin için ilk testte frame döngüsünü yazmak, kata'yı bir yazma alıştırmasına çevirir. Spare anındaki "tasarım yanlış" duygusunu yaşamadıysan, kata'nın asıl dersini kaçırdın. Ezberlediğini fark ettiğin an kısıt ekle.
Kata'yı production kodu gibi büyütmek. String Calculator'a logging, dependency injection, configuration'dan gelen ayırıcı listesi eklemek kata'yı öldürür. Kata'nın değeri küçüklüğünde; büyüdükçe döngü yavaşlar ve dikkat adımdan mimariye kayar.
Kırmızıyla refactor yapmak. Spare anında testi skip etmeden tasarımı değiştirmeye kalkışırsan, kırmızının yeni testten mi yoksa bozduğun eski davranıştan mı geldiğini ayırt edemezsin.
Süre bitince devam etmek. Zaman kutusu dolduğunda "iki dakikada bitiririm" demek, kata'yı bitirmeyi adım kalitesinin önüne koymaktır. Dur, retrospektifi yap, bir sonraki oturumda baştan başla.
Küçük Bir Deneme
★☆☆ String Calculator'ı bu kez üçüncü testte fake it ile (parts[0] + parts[1]) geçirerek tekrar çöz. İkinci adımın testi bu kez kırmızı mı? Hangi çözüm daha çok şey öğretti?
★★☆ Bowling Game'i "if yasak" kısıtıyla çöz. Frame türlerini nasıl modelledin? Testlerin değişti mi, yoksa yalnız implementasyon mu?
★★★ Ekibinle 45 dakikalık bir randori düzenle: Roman Numerals kata'sı, beş dakikalık time box, son on dakika retrospektif. Retrospektifte, kimin hangi anda adımı büyüttüğünü ve bunun kırmızıyı nasıl etkilediğini konuşun.
Bir sonraki yazıda kata'nın steril ortamından çıkıyoruz: gerçek bir endpoint için kırmızı kalan bir acceptance test'le başlayıp içeride unit seviyesinde döngüler kurarak, outside-in TDD'yi London ve classical okulları karşılaştırarak göreceğiz.