Mert Özen Her Satırda Daha İleri
Test ve Kalite TDD — #02

Red-Green-Refactor Uygulamada: Fake It, Triangulation, Obvious Implementation ve Baby Step'ler

Mert Özen 14 Kas 2026 12 dk 3 görüntülenme
Red-Green-Refactor Uygulamada: Fake It, Triangulation, Obvious Implementation ve Baby Step'ler

Kırmızı testi yeşile çevirmenin üç yolu var: kodu biliyorsan obvious implementation, bilmiyorsan fake it, tasarım belirsizse triangulation; seni şaşırtan her kırmızıda adımı küçültürsün. CouponValidator'ı xUnit v3 ile canlı geliştirirken banker's rounding sürprizini, eski testleri kıran bir fake'i, hemen yeşil olan testi bozarak doğrulamayı, TimeProvider ile İstanbul saatine göre son kullanma kuralını ve her yeşilde küçük commit ritmini gerçek çıktılarla görüyoruz.

Sürüm notu: Bu yazıdaki kodlar .NET 10, xUnit.net v3 4.0.3 ve Microsoft.Extensions.TimeProvider.Testing 10.11.0 ile derlenip denenmiştir.

Önce Tahmin Et

Önceki yazıda CouponValidator'ın test list'inden iki madde bitmişti ve listeye yeni bir madde eklenmişti: yüzde indirimi kuruşa yuvarlanmalı. Testi yazıyoruz: 99,90 liralık sepette %15 kupon. Ham sonuç 14,985. Beklenen değer olarak 14,99 yazıyoruz ve yeşile geçmek için aklımıza gelen ilk satırı ekliyoruz:

CouponKind.Percentage => Math.Round(subtotal * coupon.Value / 100m, 2),

Kod açık görünüyor: iki basamağa yuvarla. Test yeşile mi döner? Dönmezse ekranda hangi sayı görünür? Bir cevap seç, cevabı ilk döngüde göreceğiz.

Yeşile Geçmenin Üç Yolu

Kent Beck'in Test-Driven Development: By Example (2002) kitabı, kırmızı bir testi yeşile çevirmek için üç strateji tarif eder. Hepsi TDD'nin (Test-Driven Development; önce testi, sonra kodu yazma yaklaşımı) parçası; fark, ne kadar emin olduğunla ilgili.

Obvious implementation: doğru kodu biliyorsan doğrudan yazarsın. Önceki yazıdaki yüzde hesabı böyleydi. En hızlı yol budur ve deneyimli bir geliştirici zamanının çoğunu bu viteste geçirir.

Fake it: testin beklediği sonucu sabit olarak döndürürsün, sonra sabiti adım adım gerçek ifadeye dönüştürürsün. İlk bakışta saçma görünür, ama işe yarar: önce testin doğru kurulduğunu ve kodun doğru yere bağlandığını görürsün, genel çözümü sonra düşünürsün.

Triangulation: tek bir örnek genelleştirmeye yetmiyorsa, aynı davranışı farklı değerlerle soran ikinci bir test yazarsın. İki test, sabitin yetmediğini kanıtlar ve seni genel çözüme zorlar. Beck, bu yolu yalnız doğru soyutlamadan gerçekten emin olmadığında kullandığını söyler.

Seçim kuralı basit ve araba vitesine benzer: biliyorsan obvious implementation ile yaz. Ne yazacağını bilmiyorsan fake it. Tasarım hâlâ belirsizse triangulation. En önemlisi de şu: obvious implementation ile giderken seni şaşırtan bir kırmızı görürsen vites küçült. Adım boyu, belirsizliğe göre ayarlanır; emin değilsen küçült, eminsen büyüt.

Obvious Implementation ve Vites Küçültmek

Listedeki ilk madde, tutar kuponunun sepet tutarını aşmaması. 40 liralık sepete 50 liralık kupon uygulanırsa indirim 40 olmalı. Test önceki yazıdaki testlerle aynı şekilde kuruluyor; kırmızı çıktı tam olarak beklenen hatayı söylüyor:

failed Bookstore.Discounts.Tests.CouponValidatorTests.Fixed_amount_coupon_never_exceeds_the_subtotal (8ms)
  Expected: 40
  Actual:   50

Çözüm açık, doğrudan yazıyoruz:

CouponKind.FixedAmount => Math.Min(coupon.Value, subtotal),

Üç test yeşil. Sıradaki madde yuvarlama ve "Önce Tahmin Et"teki satır. Test şöyle:

[Fact]
public void Percentage_discount_is_rounded_to_cents()
{
    Coupon coupon = Coupon.Percentage("BOOK15", 15m);
    CouponValidator validator = new();

    CouponResult result = validator.Validate(coupon, subtotal: 99.90m);

    Assert.Equal(14.99m, result.Discount);
}

Önce yuvarlamasız kodla kırmızıyı görüyoruz, sonra Math.Round'lu satırı ekliyoruz. İki koşunun çıktısı:

failed Bookstore.Discounts.Tests.CouponValidatorTests.Percentage_discount_is_rounded_to_cents (2ms)
  Expected: 14.99
  Actual:   14.985

failed Bookstore.Discounts.Tests.CouponValidatorTests.Percentage_discount_is_rounded_to_cents (1ms)
  Expected: 14.99
  Actual:   14.98

İlk kırmızı beklenendi. İkincisi sürpriz: 14,985 iki basamağa 14,98 olarak yuvarlandı. Microsoft Learn'deki Math.Round dokümanı nedenini açıklıyor: mode belirtmeyen overload'lar, tam ortadaki değeri en yakın çift sayıya yuvarlar (MidpointRounding.ToEven, banker's rounding olarak da bilinir). 8 çift olduğu için 14,98.

Bu, vites küçültme anı. "Açık" sandığın kod beklemediğin bir sonuç verdi; tahmin etmeye devam etmek yerine dur ve neyi bilmediğini netleştir. Burada bilinmeyen teknik değil, iş kuralıydı: kuruş ortadaysa yukarı mı yuvarlanacak, çifte mi? Bunu ekipteki ürün sahibi ve muhasebe ile konuştuğunu varsayalım; cevap "yukarı". O zaman mode'u açıkça yazıyoruz:

CouponKind.Percentage => Math.Round(subtotal * coupon.Value / 100m, 2, MidpointRounding.AwayFromZero),

Dört test yeşil. Test, iş kuralının belirsiz kalmış bir parçasını görünür hâle getirdi ve kararı kodda kalıcı olarak belgeledi. Testi önce yazmasaydık, banker's rounding sessizce production'a gidecek ve birkaç kuruşluk farklar ay sonunda muhasebe mutabakatında ortaya çıkacaktı.

Fake It ve Triangulation: Minimum Sepet Tutarı

Sıradaki madde, minimum tutarın altındaki sepette kuponun reddedilmesi. Bu madde, önceki yazının sonunda açık kalan "reddedilen kupon için sonuç nasıl görünecek?" sorusunu da cevaplamak zorunda. Kararı testi yazarken veriyoruz: CouponResult'a nullable bir red nedeni ekleniyor, reddedilen kuponda indirim sıfır.

[Fact]
public void Coupon_is_rejected_below_its_minimum_subtotal()
{
    Coupon coupon = Coupon.Percentage("BOOK10", 10m) with { MinimumSubtotal = 150m };
    CouponValidator validator = new();

    CouponResult result = validator.Validate(coupon, subtotal: 100m);

    Assert.Equal(CouponRejection.BelowMinimumSubtotal, result.Rejection);
    Assert.Equal(0m, result.Discount);
}

with expression, factory method'ları değiştirmeden kupona isteğe bağlı bir özellik eklemenin rahat yolu. Derleme hatalarını kapatmak için Coupon'a MinimumSubtotal init property'si, CouponRejection enum'u ve CouponResult'a ikinci parametre ekliyoruz. Kırmızı: Expected: BelowMinimumSubtotal, Actual: null.

Şimdi bilerek fake it ile ilerliyoruz. İlk deneme, testteki sabit sayıyı koda kopyalamak:

if (subtotal < 150m)
{
    return new(0m, CouponRejection.BelowMinimumSubtotal);
}
failed Bookstore.Discounts.Tests.CouponValidatorTests.Fixed_amount_coupon_never_exceeds_the_subtotal (6ms)
  Expected: 40
  Actual:   0
failed Bookstore.Discounts.Tests.CouponValidatorTests.Percentage_discount_is_rounded_to_cents (0ms)
  Expected: 14.99
  Actual:   0
Test run summary: Failed!
  total: 5
  failed: 2
  succeeded: 3

Yeni test geçti ama iki eski test kırıldı: 40 ve 99,90 liralık sepetler de 150'nin altında. Canon TDD'nin üçüncü adımı bunu açıkça söyler: yeşil, yalnız yeni testin değil önceki bütün testlerin geçmesidir. Fake, sistemin geri kalanına da sahte olmamalı. Daha dar bir fake deniyoruz: minimum tutarı olan her kuponu reddet.

if (coupon.MinimumSubtotal > 0m)
{
    return new(0m, CouponRejection.BelowMinimumSubtotal);
}

Beş test yeşil. Kod açıkça yanlış, ama testler bunu henüz söylemiyor. Triangulation tam burada devreye giriyor: listedeki bir sonraki madde zaten fake'i çürütecek örnek.

[Fact]
public void Coupon_is_accepted_above_its_minimum_subtotal()
{
    Coupon coupon = Coupon.Percentage("BOOK10", 10m) with { MinimumSubtotal = 150m };
    CouponValidator validator = new();

    CouponResult result = validator.Validate(coupon, subtotal: 200m);

    Assert.Null(result.Rejection);
    Assert.Equal(20m, result.Discount);
}
failed Bookstore.Discounts.Tests.CouponValidatorTests.Coupon_is_accepted_above_its_minimum_subtotal (3ms)
  Assert.Null() Failure: Value of type 'Nullable<CouponRejection>' has a value
  Expected: null
  Actual:   BelowMinimumSubtotal

İki test birlikte, sabitin işe yaramadığını kanıtlıyor. Artık genel çözümü yazmak zorundayız ve yazdığımız şey tek bir karşılaştırma:

if (subtotal < coupon.MinimumSubtotal)

Bu örnekte genel çözüm baştan belliydi; gerçek bir projede bu kadar basit bir karşılaştırma için fake it'e gerek olmazdı. Fake it ve triangulation'ın değeri, hesabın şekli belli olmadığında ortaya çıkar: vergi dilimleri, kademeli indirim ya da birden çok kuralın birleştiği bir fiyat hesabı. O durumlarda iki üç somut örnek, genel formülü kafanda kurmaktan daha güvenilir bir yol gösterir.

Hemen Yeşil Olan Test

Listede bir sınır maddesi var: minimum tutara tam eşit sepet kabul edilir. 150 liralık sepet, 150 minimumlu kupon. Testi yazıp çalıştırınca ilk koşuda yedi test yeşil. Kırmızıyı hiç görmedik.

Bu iki anlama gelebilir. Ya davranış zaten doğru yazılmış (< operatörü eşitliği kabul ediyor) ya da test yanlış nedenle geçiyor: yanlış değişkeni kontrol ediyordur, assertion eksiktir ya da test hiç çalışmıyordur. Hangisi olduğunu anlamanın ucuz yolu, kodu bilerek bozmak. <'yi <= yapıp tekrar çalıştırıyoruz:

failed Bookstore.Discounts.Tests.CouponValidatorTests.Coupon_is_accepted_exactly_at_its_minimum_subtotal (1ms)
  Assert.Null() Failure: Value of type 'Nullable<CouponRejection>' has a value
  Expected: null
  Actual:   BelowMinimumSubtotal
Test run summary: Failed!
  total: 7
  failed: 1
  succeeded: 6

Test kırmızıya döndü, yani gerçekten sınırı koruyor. Operatörü geri alıp yeşile dönüyoruz. Bu küçük deney, mutation testing'in elle yapılmış hâli; Test Stratejisi serisinin coverage yazısı bunu araçla yapmayı anlatıyor. Hemen yeşil olan bir testi bu kontrol olmadan kabul etmek, TDD'nin en önemli garantisinden vazgeçmek demek.

TimeProvider ile Tarih Kuralı

Önceki yazıdan kalan soru: "bugün" nereden gelecek? DateTime.Now kullanan bir kural, testte bugün geçer, yarın kırılır. .NET 8 ile gelen TimeProvider soyutlaması bu iş için var; testte FakeTimeProvider ile zamanı istediğimiz ana koyabiliyoruz. Zamanla ilgili test tekniklerinin ayrıntısı Unit Test serisinin zor durumlar yazısında; burada yalnız test-first akışa odaklanıyoruz.

Testi yazmak, tasarım kararını bizim yerimize veriyor: validator zamanı constructor'dan almalı.

[Fact]
public void Expired_coupon_is_rejected()
{
    FakeTimeProvider time = new(new DateTimeOffset(2026, 11, 14, 10, 0, 0, TimeSpan.Zero));
    Coupon coupon = Coupon.Percentage("BOOK10", 10m) with { ExpiresOn = new DateOnly(2026, 11, 13) };
    CouponValidator validator = new(time);

    CouponResult result = validator.Validate(coupon, subtotal: 200m);

    Assert.Equal(CouponRejection.Expired, result.Rejection);
    Assert.Equal(0m, result.Discount);
}

Constructor'ın değişmesi, new() ile parametresiz constructor çağıran eski yedi testi de etkileyecek. Onları aynı adımda new(new FakeTimeProvider()) ile güncelliyoruz; toparlamayı refactor adımına bırakıyoruz. Production kodu henüz değişmediği için derleyici sekiz yerde CS1729 (bir argüman alan constructor yok), iki yerde de CS0117 (ExpiresOn ve Expired yok) hatası veriyor. Kodu derlenir hâle getirip kırmızıyı (Expected: Expired, Actual: null) gördükten sonra aklımıza gelen ilk implementasyonu yazıyoruz:

DateOnly today = DateOnly.FromDateTime(time.GetUtcNow().UtcDateTime);
if (coupon.ExpiresOn < today)
{
    return new(0m, CouponRejection.Expired);
}

Sekiz test yeşil. Ama listede "kupon son gününde hâlâ geçerlidir" maddesi var ve onu yazarken bir soru doğuyor: hangi günün son günü? Bookstore Türkiye'de satış yapıyor; kupon 14 Kasım'da bitiyorsa İstanbul saatiyle gece yarısına kadar geçerli olmalı. UTC (Coordinated Universal Time) ile İstanbul arasında üç saat fark var. İki testi birlikte yazıyoruz: biri İstanbul'da 23:59'da kuponun geçerli olduğunu, diğeri 00:30'da (UTC'de hâlâ 14 Kasım iken) geçersiz olduğunu söylüyor.

[Fact]
public void Coupon_expires_at_midnight_Istanbul_time_not_UTC()
{
    // 21:30 UTC = İstanbul'da ertesi gün 00:30
    FakeTimeProvider time = new(new DateTimeOffset(2026, 11, 14, 21, 30, 0, TimeSpan.Zero));
    Coupon coupon = Coupon.Percentage("BOOK10", 10m) with { ExpiresOn = new DateOnly(2026, 11, 14) };
    CouponValidator validator = new(time);

    CouponResult result = validator.Validate(coupon, subtotal: 200m);

    Assert.Equal(CouponRejection.Expired, result.Rejection);
}
failed Bookstore.Discounts.Tests.CouponValidatorTests.Coupon_expires_at_midnight_Istanbul_time_not_UTC (0ms)
  Assert.Equal() Failure: Values differ
  Expected: Expired
  Actual:   null
Test run summary: Failed!
  total: 10
  failed: 1
  succeeded: 9

23:59 testi ilk koşuda geçti (UTC'de de hâlâ 14 Kasım), 00:30 testi doğru nedenle kırmızı. Gün hesabını mağazanın saat dilimine taşıyınca on test yeşil:

private static readonly TimeZoneInfo StoreTimeZone = TimeZoneInfo.FindSystemTimeZoneById("Europe/Istanbul");

// Validate içinde
DateOnly today = DateOnly.FromDateTime(TimeZoneInfo.ConvertTime(time.GetUtcNow(), StoreTimeZone).DateTime);

Refactor Adımında Neye Bakılır

On test yeşilken iki şapkayı ayrı ayrı takıyoruz: önce production kodu, testlere dokunmadan; sonra test kodu, production koduna dokunmadan.

Production tarafında tekrar eden new(0m, CouponRejection.X) ifadeleri, red ile kabulün aynı constructor'la kurulduğunu gizliyor. İsimli factory method'lar niyeti açık söylüyor ve gün hesabı kendi method'una taşınıyor:

public sealed record CouponResult(decimal Discount, CouponRejection? Rejection)
{
    public static CouponResult Accepted(decimal discount) => new(discount, null);
    public static CouponResult Rejected(CouponRejection reason) => new(0m, reason);
}

public sealed class CouponValidator(TimeProvider time)
{
    private static readonly TimeZoneInfo StoreTimeZone = TimeZoneInfo.FindSystemTimeZoneById("Europe/Istanbul");

    public CouponResult Validate(Coupon coupon, decimal subtotal)
    {
        if (coupon.ExpiresOn < StoreToday())
        {
            return CouponResult.Rejected(CouponRejection.Expired);
        }

        if (subtotal < coupon.MinimumSubtotal)
        {
            return CouponResult.Rejected(CouponRejection.BelowMinimumSubtotal);
        }

        return CouponResult.Accepted(DiscountFor(coupon, subtotal));
    }

    private DateOnly StoreToday() =>
        DateOnly.FromDateTime(TimeZoneInfo.ConvertTime(time.GetUtcNow(), StoreTimeZone).DateTime);

    // DiscountFor değişmedi
}

Test tarafında her test aynı üç satırı tekrarlıyor: FakeTimeProvider kur, validator oluştur, çağır. Bu tekrar, bir sonraki constructor değişikliğinde on testi yeniden elden geçirmek demek. Tek bir yardımcı method'a topluyoruz ve assertion'ları da record karşılaştırmasına çeviriyoruz:

public sealed class CouponValidatorTests
{
    private static readonly DateTimeOffset NoonInIstanbul = new(2026, 11, 14, 9, 0, 0, TimeSpan.Zero);
    private static readonly Coupon Book10 = Coupon.Percentage("BOOK10", 10m);

    private static CouponResult Validate(Coupon coupon, decimal subtotal, DateTimeOffset? now = null)
    {
        CouponValidator validator = new(new FakeTimeProvider(now ?? NoonInIstanbul));
        return validator.Validate(coupon, subtotal);
    }

    [Fact]
    public void Coupon_is_rejected_below_its_minimum_subtotal()
    {
        CouponResult result = Validate(Book10 with { MinimumSubtotal = 150m }, subtotal: 100m);

        Assert.Equal(CouponResult.Rejected(CouponRejection.BelowMinimumSubtotal), result);
    }

    [Fact]
    public void Coupon_expires_at_midnight_Istanbul_time_not_UTC()
    {
        // 21:30 UTC = İstanbul'da ertesi gün 00:30
        DateTimeOffset afterMidnight = new(2026, 11, 14, 21, 30, 0, TimeSpan.Zero);

        CouponResult result = Validate(Book10 with { ExpiresOn = new DateOnly(2026, 11, 14) }, 200m, afterMidnight);

        Assert.Equal(CouponResult.Rejected(CouponRejection.Expired), result);
    }

    // Diğer sekiz test aynı kalıpta
}
Test run summary: Passed!
  total: 10
  failed: 0
  succeeded: 10
  skipped: 0
  duration: 328ms

Record karşılaştırması bir ayrıntıyı değiştirdi: kabul testleri artık yalnız indirimi değil, red nedeninin boş olduğunu da doğruluyor. Assertion'ı güçlendirmek davranış değişikliği değil, ama yine de her güçlenen testi bir kez bilerek bozup kırmızıya döndüğünü görmek iyi bir alışkanlık.

Refactor'da neye bakılacağının çerçevesi Clean Code serisinin Simple Design'ın Dört Kuralı yazısında: testler geçer, niyet açık, tekrar yok, en az eleman. Bu turda tekrar ve niyet üzerinde çalıştık. "En az eleman" kuralı ise yapmadıklarımızı açıklıyor: iki kural için ayrı rule class'ları ya da strategy pattern kurmadık. Üçüncü, dördüncü kural geldiğinde ve Validate okunmaz hâle geldiğinde o soyutlama kendini gösterecek. Tekrarı erken soyutlamak, henüz görmediğin bir değişim yönüne bahis oynamaktır.

Commit Ritmi

Her yeşilde küçük bir commit. Bu oturumun git log --oneline çıktısı:

bbcaf57 Refactor tests: one Validate helper, compare whole results
51bf43e Refactor: named CouponResult factories and StoreToday
0e0b153 Use the store's Istanbul date for coupon expiry
bd250ff Reject expired coupons using TimeProvider
72600d1 Pin the minimum subtotal boundary
afff0d1 Compare subtotal with the coupon minimum (triangulated)
de181a4 Reject coupons with a minimum subtotal (fake it)
4d8c0a2 Round percentage discount to cents, midpoint away from zero
e3269c3 Cap fixed-amount discount at the subtotal
c1c4820 Percentage and fixed-amount coupons

Bu ritmin iki faydası var. Birincisi geri dönüş noktası: bir refactor ters giderse ya da yeni bir test beklenmedik biçimde eski testleri kırarsa, son yeşile tek komutla dönersin; hatayı ayıklamak yerine adımı daha küçük tekrar atarsın. İkincisi, refactor commit'leri davranış commit'lerinden ayrı durur ve review'da "bu commit'te davranış değişmedi" demek kolaylaşır.

Fake içeren bir commit'in geçmişte durması kimseyi rahatsız etmesin; local branch senin çalışma alanın. Pull request açmadan önce bu commit'leri birleştirmek istersen Git serisinin interactive rebase ve squash yazısı tam bu iş için.

Üretimde Dikkat

Saat dilimi verisi. Europe/Istanbul gibi IANA (internet adres ve numara kayıtlarını yöneten kuruluş) kimlikleri .NET 6'dan beri Windows'ta da çalışır, ama bu dönüşüm işletim sistemindeki ICU (dil ve kültür kurallarını uygulayan Unicode kütüphanesi) kütüphanesine dayanır. Container image'larında saat dilimi verisi eksikse FindSystemTimeZoneById exception fırlatır. Testler geliştirici makinesinde geçip production'da patlamasın diye bu satırı uygulama açılışında bir kez çalıştırmak ve mağaza saat dilimini configuration'dan okumak daha sağlam.

Sunucu saati. Bu yazıda TimeProvider.GetLocalNow() yerine mağaza saat dilimini açıkça kullandık. Sunucuların çoğu UTC ile çalışır; "local" sunucunun yerelidir, müşterinin değil. Tarih kuralı içeren her testte "hangi saat diliminin günü?" sorusunu test list'e madde olarak yaz.

Yuvarlama kararları. Para hesaplayan her kodda yuvarlama mode'u bir iş kuralıdır ve testte açıkça yazılmalıdır. Bir mode'un hangi tutarlarda fark yarattığını görmek için tam ortadaki değerleri (14,985 gibi) test verisine bilinçli olarak koy.

Sık Düşülen Tuzaklar

Kırmızıyı görmeden geçmek. Hemen yeşil olan test, ya davranışın zaten var olduğunu ya da testin bir şey ölçmediğini gösterir. Hangisi olduğunu bilmeden ilerleme; kodu bir kez bozup testin kırmızıya döndüğünü gör.

Yeşilde yeni davranış eklemek. Testler yeşilken "hazır buradayken yüzde sınırını da kontrol edeyim" diye koda bir if eklemek cazip gelir. O if'i isteyen bir test yok; ileride biri onu silerse hiçbir şey kırılmaz. Yeni davranış önce listeye, sonra kırmızı bir teste, en son koda girer.

Refactor'da testleri de değiştirmek. Production kodunu refactor ederken testlere dokunursan, yeşilin anlamı kaybolur: testler mi değişti, davranış mı? İki şapkayı ayrı tak, her şapkanın sonunda testleri çalıştır.

Fake'i unutmak. Fake it ile geçilen bir testin arkasından onu çürütecek test gelmezse, sabit production'a gider. Fake yazdığın anda çürütecek örneği test list'e ekle.

Küçük Bir Deneme

★☆☆ Listede kalan "Coupon.Percentage(0) ve Percentage(120) kabul edilmez" maddesini test-first ekle. Hangi exception type'ını seçtin ve bu kararı test mi verdirdi?

★★☆ Kuponlara "yalnız hafta sonu geçerli" kuralı ekle. Test list'i önce yaz; cumartesi 00:00 ve pazar 23:59 İstanbul saatini UTC'ye çevirerek sınır testlerini kur. GetLocalNow() kullanan bir implementasyon bu testlerden hangisinde kırılır?

★★★ Kademeli bir kupon tasarla: 200 liraya kadar %5, 200–500 arası %10, 500 üstü %15. Bu kez fake it ve triangulation'ı gerçekten ihtiyaç duyarak kullan: genel formülü yazmadan önce kaç örneğe ihtiyacın oldu?

Bir sonraki yazıda üretim kodundan çıkıp kata'lara geçiyoruz: String Calculator ve Bowling Game ile bu ritmi tekrar tekrar, sonucu ezberlemeden çalışmanın yolunu göreceğiz.