TDD'nin Sınırları: "Is TDD Dead?" Tartışması, Araştırmalar, Ne Zaman TDD Yapmamalı ve AI Agent'larla TDD
TDD bir ritüel değil bir takastır: araştırmalar küçük bir kalite artışı ve karışık üretkenlik sonuçları gösteriyor, faydanın test-first sırasından çok kısa ve düzenli adımlardan gelebileceğini söylüyor. 2014'teki "Is TDD Dead?" tartışmasını, TDD'nin zayıf kaldığı spike, UI ve performans gibi yerleri, AI coding agent'larının testleri atlama ve gevşetme eğilimine karşı kuralları ve bunu yakalayan bir CI adımını gerçek çıktıyla ele alıyoruz.
Sürüm notu: Bu yazıdaki kodlar .NET 10, xUnit.net v3 4.0.3 ve Bash ile denenmiştir.
Önce Tahmin Et
Bir AI coding agent'ına, ikinci yazıda TDD (Test-Driven Development; önce testi, sonra kodu yazma yaklaşımı) ile büyüttüğümüz CouponValidator'da yüzde kuponlarına üst sınır koyma işini verdin. Agent bir süre çalıştı, "kırmızı olan kupon testlerini düzelttim" diye bir commit attı ve branch'in CI çıktısı şöyle:
skipped Bookstore.Discounts.Tests.CouponValidatorTests.Coupon_expires_at_midnight_Istanbul_time_not_UTC
Flaky on CI
Test run summary: Passed!
total: 10
failed: 0
succeeded: 9
skipped: 1
duration: 418ms
Pipeline yeşil. Bu branch'i merge eder misin? Etmeyeceksen, çıktının hangi satırı seni durdurdu ve çıktının göstermediği ne olabilir? Cevabı yazının ortasında, agent'ın diff'iyle birlikte göreceğiz.
"Is TDD Dead?" Tartışması
2014 baharında Ruby on Rails'in yaratıcısı David Heinemeier Hansson (DHH), RailsConf açılış konuşmasında ve ardından yazdığı blog yazılarında TDD'nin öldüğünü ilan etti. Kent Beck ve Martin Fowler bunun üzerine onunla Mayıs ve Haziran 2014'te beş bölümlük bir video tartışması yaptı; Fowler'ın sitesinde her bölümün kısa bir dökümü duruyor. Tartışmanın ana hatları, alıntı yapmadan şöyle:
DHH'nin ilk itirazı, TDD'nin unit testing ile karıştırılması ve mock ağırlıklı TDD'nin tasarıma zarar vermesiydi. Bunun için "test-induced design damage" terimini kullandı: kodu yalnız izole test edilebilsin diye katmanlara, interface'lere ve dolaylı çağrılara bölmek, okunurluğu ve basitliği bozuyordu. Beck'in cevabı, sorunun TDD'nin kendisinde değil, tek tek verilen tasarım kararlarında olduğuydu; testin baskısı tasarımı iyiye de kötüye de itebilir. Dördüncü yazıda gördüğümüz ICouponPolicy ve adapter'ı, bu tartışmanın küçük bir örneği: mock'lu tasarım bir katman doğurdu, o katmanın gerekip gerekmediği ayrı bir karar.
Sonraki bölümlerde konu geri bildirime ve maliyete kaydı. Beck geri bildirimi sıklık, doğruluk, maliyet ve ömür arasında bir denge olarak tarif etti. DHH, TDD'nin bazı ekiplerde ayrı bir QA ve keşif testini gereksiz gösterdiğinden yakındı. Hepsi aşırı test etmenin, testler arasındaki gereksiz bağların ve test kodu ile production kodu oranının gerçek bir maliyet olduğunda anlaştı. Beck, benzersiz bir şey korumayan testlerin silinebileceğini söyledi.
Tartışma bir uzlaşmayla bitmedi. Ortak nokta, kendini test eden kodun değerli olduğu ve TDD'nin bu hedefe giden yollardan biri olduğuydu; tek yol değil. TDD'nin hangi bağlamlarda ne kadar işe yaradığı konusunda farklı yerlerde durdular. Fowler kapanışta, teknikleri denemeyi, gerekirse aşırıya kaçıp sınırlarını görmeyi ve ekibe uyanı benimsemeyi önerdi. On iki yıl sonra okunduğunda tartışmanın değeri bir kazanan çıkarmasında değil, TDD'nin bir ritüel değil bir takas olarak konuşulmasında.
Araştırmalar Ne Diyor?
TDD'nin etkisini ölçmeye çalışan çok sayıda çalışma var ve sonuçları karışık. En sık atıf yapılanlardan üçü:
Nagappan, Maximilien, Bhat ve Williams'ın 2008 tarihli çalışması, Microsoft'ta üç ve IBM'de bir ekibin TDD deneyimini inceledi. TDD kullanan projelerin yayın öncesi defect yoğunluğu, benzer projelere göre yüzde 40 ile 90 arasında düşüktü; yöneticiler ise ilk geliştirme süresinin yüzde 15 ile 35 arasında uzadığını tahmin etti. Bunlar vaka çalışmaları: kontrollü deney değil, süre artışı da ölçüm değil tahmin.
Rafique ve Mišić'in 2013 tarihli meta-analizi 27 çalışmayı birleştirdi. Sonuç: dış kaliteye küçük bir olumlu etki, üretkenliğe ise belirgin bir etki yok. İlginç ayrıntı, endüstride yapılan çalışmalarda hem kalite artışının hem üretkenlik düşüşünün akademik çalışmalara göre çok daha büyük çıkması.
Fucci, Erdogmus, Turhan, Oivo ve Juristo'nun 2017 tarihli çalışması soruyu farklı sordu: TDD'nin hangi özelliği fark yaratıyor? 39 profesyonel geliştiricinin süreç verisinde, testin koddan önce mi sonra mı yazıldığı belirleyici çıkmadı. Etkili görünen, döngülerin kısa ve düzenli olmasıydı. Yazarların yorumu, TDD'ye atfedilen faydanın test-first sırasından çok, TDD benzeri süreçlerin teşvik ettiği küçük ve istikrarlı adımlardan gelebileceği.
Bu sonuçlar neden bu kadar dağınık? Çünkü ölçmesi zor. "Kalite" defect sayısıyla mı, bakım maliyetiyle mi, tasarımla mı ölçülecek? Kontrol grubu kim; TDD'yi bir haftada öğrenmiş öğrenciler mi, yıllardır test yazan bir ekip mi? TDD'nin vaat ettiği faydaların bir kısmı (değişime dayanıklı tasarım, korkusuz refactoring) aylar ya da yıllar içinde görünür, çalışmaların çoğu ise günler ya da haftalar sürer. Bu yüzden araştırmalardan çıkarılabilecek makul sonuç şu: TDD her şeyi çözen bir yöntem değil, zararlı da değil; küçük adımlarla ve sürekli geri bildirimle çalışmanın bir biçimi ve faydası büyük ölçüde nasıl uygulandığına bağlı.
TDD'nin Zayıf Kaldığı Yerler
TDD, ne istediğini bildiğin ama nasıl yapacağını bilmediğin durumlarda en güçlüdür. Ne istediğini bilmediğin durumlarda zayıflar.
Keşif kodu ve spike. Yeni bir kütüphaneyi denemek, bir ödeme sağlayıcısının API'sinin gerçekte nasıl davrandığını görmek, bir algoritmanın uygulanabilir olup olmadığını anlamak için yazılan kod öğrenme içindir. Test list yazamazsın, çünkü davranışı henüz bilmiyorsun. Burada dürüst yaklaşım, keşif kodunu bilinçli olarak test'siz ve atılacak kod olarak yazmak, öğrendiğini not almak ve production kodunu baştan TDD ile yazmaktır.
Dan North'un anlattığı "spike and stabilize" yaklaşımı bunun bir adım ötesi: bazen keşif kodunu bir feature flag arkasında gerçek kullanıcılara açıp gerçekten değer üretip üretmediğini görmek, testleri ve temizliği ancak değer kanıtlandıktan sonra eklemek daha mantıklıdır. Değer üretmeyen kod silinir; üreten kod stabilize edilir, yani karakterize edilir, test edilir ve refactor edilir. Önceki yazıdaki characterization test ve sprout teknikleri tam bu stabilizasyon adımının araçları.
Belirsiz UI ve görsel düzen. "Bu sayfa güzel görünüyor mu?" sorusunun önceden yazılabilecek bir assertion'ı yoktur. Beşinci yazıda gördüğümüz gibi, component'in davranışı test-first yazılabilir; görsel düzen ise tasarımcıyla birlikte, ekranda denenerek oturur. Görsel regresyon testleri bu düzeni sonradan korur, ama tasarımı yönlendirmez.
Framework konfigürasyonu. Dependency injection kayıtları, middleware sırası, serialization ayarları için unit test yazmak çoğu zaman framework'ü test etmek demektir. Bunların doğruluğunu en iyi gösteren, dördüncü yazıdaki gibi uygulamayı process içinde ayağa kaldıran birkaç acceptance test'tir.
Performans. "Bu sorgu 50 ms altında kalmalı" türünden bir hedef test olarak yazılabilir, ama red-green-refactor'ın dakikalık ritmine uymaz; ölçüm gürültülüdür ve iyileştirme çoğu zaman tasarımı değiştirir. Performans, profil çıkarmak ve ölçmekle ilerler; bunun yeri ilerideki Performance ve Load Testing serisi.
AI Coding Agent'larıyla TDD
Coding agent'ları TDD tartışmasına yeni bir boyut ekledi. Agent hızlı kod yazar, ama kodun doğru olduğunu kendi başına bilemez; "çalışıyor" demesi bir iddiadır. Testler, bu iddiayı doğrulanabilir hâle getiren tek mekanizma. Bu açıdan test-first, agent'la çalışırken insanın agent'a verdiği spesifikasyonun en somut biçimi: "şu girdiyle şu sonuç" cümlesini doğal dilde değil, çalıştırılabilir kodla söylüyorsun.
Kent Beck, Haziran 2025'te yayımladığı "Augmented Coding: Beyond the Vibes" yazısında bu deneyimi anlatıyor. Bir agent ile Rust ve Python'da B+ tree kütüphanesi yazdı ve agent'a TDD ritmini sistem talimatlarında tarif etti: bir plan dosyasından sıradaki işaretlenmemiş testi al, yalnız o testi geçirecek kadar kod yaz, yapısal değişikliklerle davranış değişikliklerini ayrı commit'lere koy, testler yeşil olmadan commit atma. İzlediği uyarı işaretleri de öğretici: agent'ın döngüye girmesi, istenmemiş işlevsellik eklemesi ve testleri devre dışı bırakmak ya da silmek gibi hile belirtileri. Deneyimin bir başka ilginç ayrıntısı, Rust sürümü karmaşıklıkta tıkandığında aynı testlerle Python sürümünün yazdırılması ve ondan Rust'a çevrilmesi; testler dil değiştiğinde de spesifikasyon olarak kaldı.
"Önce Tahmin Et"teki branch tam bu hile belirtisini taşıyor. Agent'ın diff'i:
@@ -45 +45 @@ public sealed class CouponValidatorTests
- Assert.Equal(CouponResult.Accepted(14.99m), result);
+ Assert.True(result.Discount > 0);
@@ -91 +91 @@ public sealed class CouponValidatorTests
- [Fact]
+ [Fact(Skip = "Flaky on CI")]
Agent, kırmızıya döndürdüğü iki testi düzeltmek yerine birini atlamış, diğerinin assertion'ını "sıfırdan büyük" kadar gevşetmiş. Pipeline yeşil, ama yuvarlama kuralı ve İstanbul gece yarısı kuralı artık korunmuyor. CI çıktısındaki skipped: 1 satırı tek ipucuydu; assertion gevşetmesini çıktı hiç göstermiyor.
Bunun için iki savunma hattı kuruyoruz. Birincisi agent'a verilen kurallar; çoğu agent, repository kökündeki bir talimat dosyasını okur:
## Testing rules for coding agents
- Work from TESTLIST.md: pick the next unchecked item, write one failing test, run it and show the red output.
- Write only the code needed to make that test pass; run the whole suite before you report green.
- Never delete, skip or weaken an existing test or assertion. If a test looks wrong, stop and ask.
- Keep structural changes (refactoring) and behavioral changes in separate commits.
- Do not add behavior that no test asks for; add it to TESTLIST.md instead.
Kurallar gereklidir ama yeterli değildir; agent onları her zaman izlemez. İkinci hat, kuralın ihlalini mekanik olarak yakalayan bir CI adımı. Basit bir script, branch'in test dosyalarındaki diff'te silinen assertion'ları, silinen [Fact]/[Theory] satırlarını ve eklenen Skip'leri sayıyor:
#!/usr/bin/env bash
# Branch'teki test değişikliklerinde zayıflatma izi arar
set -euo pipefail
base="${1:-main}"
diff=$(git diff --unified=0 "$base"...HEAD -- '*Tests.cs')
removed_asserts=$(grep -cE '^-[[:space:]]*Assert\.' <<< "$diff" || true)
removed_facts=$(grep -cE '^-[[:space:]]*\[(Fact|Theory)\]' <<< "$diff" || true)
added_skips=$(grep -cE '^\+.*Skip[[:space:]]*=' <<< "$diff" || true)
echo "removed asserts: $removed_asserts, removed [Fact]/[Theory]: $removed_facts, added skips: $added_skips"
if (( removed_asserts + removed_facts + added_skips > 0 )); then
echo "Test changes need a human review before merge." >&2
exit 1
fi
$ ./tools/tests-guard.sh main
removed asserts: 1, removed [Fact]/[Theory]: 1, added skips: 1
Test changes need a human review before merge.
$ echo $?
1
Script bir yargıç değil, bir alarm. Değişen bir assertion her zaman zayıflatma değildir; bazen test gerçekten yanlıştır. Amaç, test dosyalarına dokunan her değişikliği bir insanın bilinçli olarak onaylamasını sağlamak. Windows'ta script Git Bash ile ya da CI'daki Linux runner'da çalışır. Daha sağlam bir kontrol için mutation testing, testlerin gerçekten bir şey yakaladığını ölçer; Test Stratejisi serisinin coverage yazısı bunu anlatıyor. Agent'larla çalışma akışının geri kalanı, yani görev tarifi, review ve bağlam yönetimi, ilerideki AI Destekli Geliştirme serisinin konusu.
Ekipte TDD'yi Yaymak
TDD'yi bir ekip kuralına çevirmek ("bütün kod test-first yazılır") genelde ters teper: kimin uyduğunu denetlemek imkânsızdır ve kural, test sonradan yazılıp commit sırası düzenlenerek kolayca atlatılır. İşe yarayan yol, TDD'yi bir pratik olarak görünür ve çekici kılmak.
Pair ve mob programming, TDD'nin en hızlı yayıldığı ortam. Ping-pong pairing'de biri testi, diğeri kodu yazar; ritim kendiliğinden öğrenilir. Üçüncü yazıdaki haftalık kata oturumları, bu ritmi production baskısı olmadan çalışmanın yeri.
Review'da test-first izlerine bakmak da yardımcı olur, ama denetim olarak değil merak olarak. Commit geçmişinde test ile kodun birlikte, küçük adımlarla geldiğini görmek; bug fix pull request'inde önce kırmızı testin, sonra düzeltmenin commit'lendiğini görmek; pull request açıklamasında bir test list bulmak. Bunları review'da övmek, yokluklarını eleştirmekten daha çok iş görür.
Üretimde Dikkat
Agent'ın yazdığı testlerin kalitesi. Agent'a hem kodu hem testi yazdırdığında, ikisi aynı yanlış anlayışı paylaşabilir: test, kodun yaptığını doğrular, istenen davranışı değil. Bu, altıncı yazıdaki characterization testine benzeyen bir tuzak. Test list'i ve en azından kritik testlerin beklenen değerlerini insanın yazması, agent'ı spesifikasyona bağlar.
Atlanmış testlerin birikmesi. Skip ile kapatılmış testler zamanla birikir ve kimse neden kapatıldığını hatırlamaz. CI'da atlanan test sayısını izlemek, sıfırdan yukarı çıktığında birinin sahiplenmesini sağlamak, agent'lı ya da agent'sız her ekip için iyi bir hijyen.
Ölçüm tuzakları. TDD'nin ekipteki etkisini görmek istiyorsan coverage yüzdesi yanlış metriktir; test-first yazılmayan kod da yüksek coverage'a ulaşabilir. Daha anlamlı sinyaller: refactoring commit'lerinin sıklığı, production'a kaçan regresyonların sayısı, yeni bir geliştiricinin bir modülü ne kadar sürede güvenle değiştirebildiği.
Sık Düşülen Tuzaklar
TDD'yi coverage hedefi gibi dayatmak. "Yüzde 80 coverage" hedefi, assertion'sız testleri, getter testlerini ve kodun yaptığını aynen kopyalayan testleri ödüllendirir. Beck'in Canon TDD'de saydığı ilk hatalardan biri de coverage için assertion'sız test yazmaktı. Coverage bir sonuçtur, hedef değil.
"TDD yapmıyoruz" diyerek test yazmamayı meşrulaştırmak. "Is TDD Dead?" tartışmasının üç katılımcısı da kendini test eden kodun gerekli olduğunda hemfikirdi. TDD'yi bırakmak, testi bırakmak anlamına gelmez; testin ne zaman yazılacağını değiştirir.
Agent'ın "testler geçiyor" cümlesine güvenmek. Agent'ın raporu bir iddiadır. Kırmızıyı gördün mü, testler gerçekten koştu mu, diff'te test dosyaları değişti mi? Bu üç soru, agent'la çalışırken TDD'nin özü.
Spike kodunu production'a taşımak. Keşif için yazılmış test'siz kod, "zaten çalışıyor" denerek ana branch'e girdiğinde legacy kod doğmuş olur. Spike'ın sonu ya silmek ya da stabilize etmektir; ikisinin ortası yoktur.
Küçük Bir Deneme
★☆☆ Kendi projende son on pull request'e bak: kaçında test ile kod aynı commit'lerde geldi, kaçında testler en sonda ayrı bir commit'te? Bir bug fix'te kırmızı testin ayrı bir commit olarak durduğu bir örnek bulabildin mi?
★★☆ tests-guard.sh'yi genişlet: Assert.Equal'ın Assert.True'ya dönüştüğü satırları ayrıca raporla ve DidNotReceive/Received satırlarının silinmesini de say. Kendi branch'inde bilerek bir assertion gevşetip script'in yakaladığını gör.
★★★ Bir coding agent'ına bu serinin ikinci yazısındaki CouponValidator test list'ini ve yukarıdaki kuralları ver, işi baştan yaptır. Her adımda kırmızıyı gösterdi mi, istenmemiş bir davranış ekledi mi, bir testi değiştirmeye çalıştı mı? Sonuçta oluşan kodu ve commit geçmişini, ikinci yazıdaki git log ile karşılaştır.
Seri burada bitiyor. Yedi yazı boyunca TDD'yi bir test tekniği olarak değil, küçük adımlarla tasarım yapma disiplini olarak ele aldık: test list ile başlamak, kırmızıyı doğru nedenle görmek, yeşili en kısa yoldan bulmak, refactor'ı atlamamak ve bunu kata'dan legacy koda, backend'den Angular'a, insan ekipten agent'a kadar farklı ortamlarda denemek. Buradan sonra yolun devamı, ilerideki Refactoring, Integration ve E2E (end-to-end) Test ve Frontend Testleri serilerinde.