TDD(Test Driven Development) Nedir?

“Test Driven Development” (Test Güdümlü Geliştirme) metodolojisini benimseyen ve bu metodolojiyi uygulayan yazılım geliştiricileri tanımlamak için kullanılan bir terimdir. TDD, yazılım geliştirme sürecinde testlerin yazılımın geliştirilmesine öncelik verdiği bir yaklaşımdır.

TDD Adımları Nelerdir?

Test Yazımı: Geliştirici, önce yapılacak işlevsellik için birim testini yazar. Bu test, henüz geliştirilmemiş bir işlevselliği test eder. Test yazıldıktan sonra çalıştırılır ve doğal olarak başarısız olur çünkü henüz işlevsellik mevcut değildir.

Kod Yazma: Geliştirici, yazdığı testin geçmesi için gereken en basit kodu yazar. Bu kod, yalnızca testin geçmesini sağlamak için minimum düzeyde olur.

Testin Geçirilmesi: Kod yazıldıktan sonra test tekrar çalıştırılır. Testin başarıyla geçmesi, geliştiricinin kodunun doğru çalıştığını gösterir.

Refactoring (İyileştirme): Testlerin geçtiğinden emin olunduktan sonra, geliştirici kodu optimize eder ve daha temiz, daha verimli hale getirir. Bu süreçte, testler sürekli olarak çalıştırılır ve kodun hala testleri geçtiğinden emin olunur.

Tekrar: Bu döngü, tüm geliştirme süreci boyunca tekrarlanır; her yeni işlevsellik için önce testler yazılır, ardından kod geliştirilir, testleri geçene kadar bu döngü devam eder.

Temel amaç kaliteyi artırmak, hataları minimize etmektir. TDD, geliştiricilerin kodu yazmadan önce testler oluşturmasını ve bu testlerin kılavuzluğunda kod yazmasını öngörür. Bu yaklaşım, yazılım geliştirme sürecinde daha güvenilir ve bakımı kolay bir kod tabanı oluşturmayı hedefler. Bu makalede, TDD’nin temel prensiplerini, sağladığı faydaları ve basit uygulama örnekleri ile birlikte detaylı olarak ele alacağız.

TDD döngüsü her yeni işlevsellik için tekrarlanır ve yazılım geliştikçe bu döngü sürekli olarak devam eder. Her zaman testlerle güvence altına alınmış bir kod tabanı oluşturarak süreç devam eder.

TDD’nin Faydaları

Daha Az Hata: Testler yazılıp kod testlerden sonra geliştirildiği için, hataların çoğu yazılım geliştirme sürecinde erkenden tespit edilir ve düzeltilir. Bu, ürünün nihai sürümünde daha az hata ile karşılaşılmasını sağlar.

Daha İyi Tasarım: TDD, geliştiricileri küçük ve iyi tanımlanmış işlevsellikler geliştirmeye teşvik eder. Bu, kodun daha modüler ve bakımı kolay hale gelmesini sağlar.

Kodun Güvenilirliği: TDD süreci boyunca yazılan testler, kodun gelecekte yapılacak değişiklikler karşısında sağlam kalmasını sağlar. Bu, kodun güvenilirliğini artırır ve gelecekte yapılacak değişikliklerin mevcut işlevselliği bozup bozmadığını kontrol etmeyi kolaylaştırır.

Geliştirilmiş Dokümantasyon: Testler, kodun nasıl çalıştığını ve hangi durumlarda hangi çıktıyı vermesi gerektiğini belgeleyen doğal bir dokümantasyon görevi görür. Bu, yeni geliştiricilerin projeye katılmasını ve mevcut kodu anlamasını kolaylaştırır.

Uygulamalı Örnekler

To-Do List (Yapılacaklar Listesi) Uygulaması

Adım 1: Başarısız Bir Test Yazma

İlk olarak, yapılacak işlerin listeye eklenmesini test etme işlevini yazalım. Bu test, henüz “add_task” fonksiyonunu geliştirmediğim için başarısız olacaktır.

def test_add_task():
    todo = ToDoList()
    todo.add_task("Python öğren")
    assert "Python öğren" in todo.tasks

Bu, test “Python öğren” adında bir görevin “ToDoList” adlı sınıfa eklendiğini doğrular. Ancak, “add_task” fonksiyonu henüz yazılmadığı için bu test başarısız olur.

Adım 2: Kod Yazma

Testin geçmesi için gerekli olan minimum kodu yazalım.

class ToDoList:
    def __init__(self):
        self.tasks = []
    
    def add_task(self, task):
        self.tasks.append(task)

Bu kod, “add_task” fonksiyonunu basit bir şekilde tanımlar ve görevi “tasks” listesine ekler. Testi tekrar çalıştırdığımızda bu kodun testi geçmesini beklenir.

Adım 3: Kodun İyileştirilmesi

Kodun ilk versiyonu çalışır durumda, ancak daha iyi hale getirmek için iyileştirmeler yapabiliriz. Örneğin, aynı görevin birden fazla kez eklenmesini engellemek isteyebiliriz. Bu durumda, “add_task” fonksiyonunu aşağıdaki gibi değiştirebiliriz.

def add_task(self, task):
    if task not in self.tasks:
        self.tasks.append(task)

Bu değişiklikle, aynı görev birden fazla kez eklenmeyecektir. Kodun bu yeni hali, hem işlevsel hem de daha güvenilirdir.

TDD Metodları

Outside-In TDD (Büyütme Yöntemi)

Outside-In TDD, yazılımın en dış katmanlarından (örneğin, kullanıcı arayüzü veya API) başlayarak iç katmanlarına doğru testler ve kod yazmayı içeren bir yaklaşımdır. Bu metod, genellikle müşteri veya kullanıcı gereksinimlerinden yola çıkılarak, sistemin kullanıcıya en yakın parçasından başlayıp, alt sistemler ve bağımlılıklara doğru inilir.

Adımları:

  1. Öncelikle, kullanıcı gereksinimlerine dayalı dış katman (UI veya API) için testler yazılır.
  2. Ardından, bu testleri geçirecek minimum düzeyde dış katman kodu yazılır.
  3. Bu katman kodu, gerekli iç katmanlara (servisler, veri erişim katmanı vb.) ihtiyaç duyacaktır. İç katmanlar için testler yazılır ve bu döngü en iç katmanlara kadar devam eder.

DIŞ KATMAN TESTİ (UI veya API)

def test_user_can_login():
    response = login_user("[email protected]", "password123")
    assert response == "Login successful"

API KATMANI KODU

def login_user(email, password):
    user = User.authenticate(email, password)
    if user:
        return "Login successful"
    else:
        return "Login failed"

İÇ KATMAN TESTİ (Servis veya Model)

def test_user_authentication():
    user = User(email="[email protected]", password="password123")
    assert user.authenticate("password123") is True

İÇ KATMAN KODU

class User:
    def __init__(self, email, password):
        self.email = email
        self.password = password

    def authenticate(self, password):
        return self.password == password

Mocking ve Stubbing ile TDD

Mocking ve stubbing, TDD’nin önemli tekniklerinden biridir. Bu metod, bağımlılıkları olan birimleri test edilmesini kolaylaştırmak için kullanılır.

Mocking: Bir sınıfın veya modülün, başka bir sınıfa bağımlı olduğu durumlarda, bağımlı sınıfın davranışını taklit eden sahte(mock) bir sınıf oluşturulur. Bu sahte sınıf, belirli girdilerle çağrıldığında belirli çıktıları verir. Bu, testin belirli bir davranışı izole ederek test etmesini sağlar.

Stubbing: Bir metodun belirli bir çağrıda bulunulduğunda önceden belirlenmiş bir değer döndürmesini sağlayan bir tekniktir. Stubbing, bağımlı sınıfların veya metodların yerine geçici, kontrol edilebilir ve tahmin edilebilir sahte metodlar koymak için kullanılır.

Mocking ve stubbing, özelikle dış bağımlılıkları olan sistemler için kritik öneme sahiptir(örneğin, veritabanları, web servisleri). Bu teknikler, bu bağımlılıklardan bağımsız olarak birimlerin test edilmesini sağlar.

Dış bağımlılıkları olan bir sistemi test ederken mock’lar ve stub’lar kullanarak nasıl izole testler yapılabileceğini gösterelim:

Adımları:

TEST YAZMA (Mock Kullanarak)

from unittest.mock import Mock

def test_get_user_data():
    api_client = Mock()
    api_client.get_data.return_value = {"name": "Yusuf Dalbudak"}
    
    user_data = get_user_data(api_client)
    assert user_data["name"] == "John Doe"

KOD YAZMA

def get_user_data(api_client):
    return api_client.get_data()

REFACTOR (Gerekirse İyileştirme Yapılır)

Bu adımda genellikle kodun temizlenmesi veya performansının artırılması sağlanır. Ancak, örnekteki kod oldukça basit olduğundan iyileştirme gerekmez.

Behavior-Driven Development (BDD) ile TDD

Behavior-Driven Development (BDD), TDD’nin bir uzantısı olarak görülebilir. BDD, testlerin “davranışlar” üzerinden tanımlanmasını sağlar ve iş gereksinimlerini yazılım testlerine dönüştürür. Bu metod, yazılımın nasıl çalışması gerektiğini, genellikle iş diliyle tanımlanmış testlerle doğrulamaya odaklanır.

Gherkin Syntax: BDD’de genellikle Gherkin adlı bir syntax kullanılır. Gherkin, “Given-When-Then” formatında yazılır:

Given: Başlangıç durumu.

When: Gerçekleşen eylem.

Then: Beklenen sonuç veya davranış.

Bir hesap makinesi uygulaması için BDD yaklaşımıyla bir test örneği gösterelim.

ADIMLAR

TEST YAZMA (Given-When-Then)

def test_addition():
    # Given
    calculator = Calculator()
    
    # When
    result = calculator.add(2, 3)
    
    # Then
    assert result == 5

KOD YAZMA

class Calculator:
    def add(self, a, b):
        return a + b

Acceptance Test-Driven Development (ATDD)

Acceptance Test-Driven Development (ATDD), müşteri veya kullanıcı kabul testlerinin yazılım geliştirme sürecine entegre edilmesi metodudur. Bu metod, iş gereksinimlerinin karşılanıp karşılanmadığını doğrulayan testlerin, yazılım geliştirme sürecinin başında yazılmasını içerir.

Adımlar

KABUL TESTİ

Müşteri, İş Analisti ve geliştirici birlikte kabul kriterlerini belirler.

def test_user_registration():
    response = register_user("[email protected]", "password123")
    assert response == "Registration successful"

Kod Yazma

Bu kabul kriterlerine dayalı olarak testler yazılır.

def register_user(email, password):
    return "Registration successful"

Yazılım, bu testlerin geçmesi için geliştirilir.

Unit Testing TDD

Unit testing TDD, TDD’nin en yaygın kullanılan şeklidir ve yazılımın en küçük bileşenlerini (birimlerini) test etmeye odaklanır. Bu metod, her birimin (fonksiyon, metod, sınıf) bağımsız olarak test edilmesini sağlar.

Adımlar:

TEST YAZMA

Bir fonksiyon veya metod için bir test yazılır.

def test_square():
    result = square(4)
    assert result == 16

KOD YAZMA

Testin geçmesi için gereken minimum kod yazılır.

def square(x):
    return x * x

Kod iyileştirilir ve testler tekrar çalıştırılır.

Integration Testing TDD

Integration testing TDD, yazılım bileşenlerinin birbiriyle nasıl etkileşime girdiğini test etmeye odaklanır. Bu metod, bağımsız olarak test edilmiş birimlerin bir araya getirildiğinde nasıl çalıştığını doğrular.

Adımlar:

TEST YAZMA (Entegrasyon Testi)

Bileşenler arası etkileşimleri test eden testler yazılır.

def test_user_creation_and_retrieval():
    user = create_user("[email protected]", "password123")
    retrieved_user = get_user("[email protected]")
    assert user.email == retrieved_user.email

KOD YAZMA

Bu testlerin geçmesi için gerekli entegrasyon kodu yazılır.

users_db = {}

def create_user(email, password):
    user = User(email, password)
    users_db[email] = user
    return user

def get_user(email):
    return users_db.get(email)

class User:
    def __init__(self, email, password):
        self.email = email
        self.password = password

Entegrasyon kodu iyileştirilir ve tüm testler çalıştırılır.

MAKİNE ÖĞRENMESİ ve TDD

Test Driven Development (TDD), geleneksel yazılım geliştirme süreçlerinde yaygın olarak kullanılan bir metodoloji olmasına rağmen, makine öğrenimi (ML) projelerinde uygulanması biraz daha karmaşıktır. Bununla birlikte, TDD’nin makine öğrenimi projelerine adapte edilmesi mümkündür ve bu, modellerin doğruluğunu ve güvenilirliğini artırmada faydalı olabilir.

İşte bazı yöntem ve stratejiler:

Veri Doğrulama Testleri

Makine öğrenimi projelerinin temelini veri oluşturur. Veri kalitesi, modelin performansını doğrudan etkiler. TDD, verilerin doğruluğunu sağlamak için kullanılabilmektedir.

Örnek Bir Veri Doğrulama Testi

Bu test, verilerin belirli kalite standartlarını karşıladığından emin olmayı sağlar. Veriler temiz ve doğru olduğunda, model eğitimine geçilebilir.

def test_data_integrity():
    data = load_data("data.csv")
    
# Test: Boş değerlerin olmaması
    assert data.isnull().sum().sum() == 0

    # Test: Doğru veri türlerinin olması
    assert data["age"].dtype == int
    assert data["income"].dtype == float

Özellik (Feature) Mühendisliği Testleri

Makine öğrenimi projelerinde, özellik mühendisliği kritik bir adımdır. TDD, bu süreçte oluşturulan özelliklerin doğruluğunu test etmek için kullanılabilir.

Bu testler, özellik mühendisliği sırasında oluşturulan özelliklerin doğru hesaplandığından emin olmayı sağlar.

def test_feature_creation():
    data = load_data("data.csv")
    data = create_features(data)
   
 # Test: Yeni özelliklerin varlığı
    assert "age_squared" in data.columns
    assert "income_per_person" in data.columns
    
# Test: Özelliklerin doğruluğu
    assert data["age_squared"].equals(data["age"] ** 2)
    assert data["income_per_person"].equals(data["income"] / data["household_size"])

Model Eğitimi ve Performans Testleri

TDD, model eğitimi ve performansını doğrulamak için de kullanılabilir. Bu testler, modelin beklenen performansı gösterip göstermediğini kontrol eder.

Bu test, modelin belirli bir doğruluk eşiğini geçip geçmediğini kontrol eder. Bu tür testler, modelin eğitimi sırasında performansını sürekli izlemeyi sağlar.

def test_model_training():
    X_train, X_test, y_train, y_test = train_test_split(data, target, test_size=0.2)
    model = train_model(X_train, y_train)
    predictions = model.predict(X_test)
    # Test: Model doğruluğu (accuracy)
    accuracy = accuracy_score(y_test, predictions)
    assert accuracy > 0.8

Model Doğruluğunu Koruma (Regression Testing)

Bir model üzerinde yapılan değişikliklerin mevcut performansı etkilemediğinden emin olmak için TDD kullanılarak regresyon testleri uygulanabilir.

Bu testler, modelin güncellenmiş sürümlerinin eski sürümlere kıyasla beklenen performansı koruyup korumadığını kontrol eder.

#Regresyon Testi

def test_model_regression():
    old_model = load_model("model_v1.pkl")
    new_model = load_model("model_v2.pkl")
    X_test = load_data("test_data.csv")
    
    old_predictions = old_model.predict(X_test)
    new_predictions = new_model.predict(X_test)
    
    assert new_model.score(X_test, y_test) >= old_model.score(X_test, y_test)

DATA Pipelines İçin Testler

Makine öğrenimi projelerinde veri işleme boru hatları (data pipelines) yaygın olarak kullanılır. Bu boru hatlarının her adımının doğru çalıştığından emin olmak için TDD kullanılabilir.

Bu test, veri boru hattının her adımının doğru çalıştığını ve beklenen sonuçları ürettiğini doğrular.

def test_data_pipeline():
    pipeline = create_data_pipeline()
    processed_data = pipeline.fit_transform(raw_data)
    
    # Test: Verilerin doğru şekilde işlenmesi
    assert processed_data.isnull().sum().sum() == 0
    assert processed_data.shape[1] == expected_number_of_features

Model Çıktılarının Doğrulanması

Modelin verdiği kararların belirli kurallara uygun olup olmadığını kontrol eden testler yazılabilir. Örneğin, bir sınıflandırma modelinin belirli sınıfları ayırt etme kapasitesini kontrol etmek isteyebilirsiniz. Bu test, modelin belirli bir durumda beklenen çıktıları ürettiğini kontrol eder.

def test_model_outputs():
    model = train_model(data)
    predictions = model.predict(new_data)
    
    # Test: Modelin belirli bir sınıfı tanıyabilmesi
    assert "important_class" in predictions

Modelin Üretime Geçirilmesi için Testler

Modelin üretim ortamına geçmeden önce tüm bileşenlerin doğru çalıştığını doğrulamak için entegrasyon testleri yapılabilir.

Bu test, modelin üretime hazır olduğundan ve istenilen performansı gösterdiğinden emin olmak için yapılır.

def test_model_deployment():
    deployed_model = deploy_model("model_v2.pkl")
    
    # Test: Modelin API üzerinden düzgün çalışıp çalışmadığı
    response = deployed_model.predict([input_data])
    assert response == expected_output

SONUÇ

Test Driven Development (TDD), yazılım geliştirme sürecinde kaliteyi artıran ve hataları minimize eden güçlü bir metodolojidir. TDD, yazılım geliştirme süreçlerini daha düzenli, sürdürülebilir ve güvenilir hale getirirken, kodun modülerliğini ve bakımı kolaylaştırır. Bu metodoloji, yalnızca geleneksel yazılım projelerinde değil, aynı zamanda makine öğrenimi projelerinde de büyük faydalar sağlar.

Makine öğrenimi projelerinde TDD’nin uygulanması, verilerin doğruluğundan model performansına kadar her aşamada güvenilirliği artırır. Veri doğrulama testleri, özellik mühendisliği testleri ve model eğitimi gibi adımlar, TDD’nin makine öğrenimi süreçlerine entegre edilmesiyle daha güvenli ve sağlam hale gelir. Ayrıca, regresyon testleri ve veri işleme boru hatları için yapılan testler, modelin gelecekteki değişiklikler karşısında kararlılığını garanti altına alır.

TDD’nin sunduğu bu avantajlar, yazılım geliştiricilerin ve veri bilimcilerin daha az hata ile daha güvenilir ve sürdürülebilir projeler üretmesini sağlar. Bu metodoloji, yazılım geliştirme sürecinin her aşamasında kaliteyi artırarak, sonuçta daha güçlü ve başarılı yazılım ürünleri ortaya çıkmasına katkı sağlar. TDD’nin temel prensiplerini benimsemek ve bu metodolojiyi yazılım geliştirme süreçlerine entegre etmek, uzun vadede projelerin başarısını güvence altına almanın en etkili yollarından biridir.

KAYNAKÇA

Beck, K. (2002). Test-Driven Development: By Example. Addison-Wesley Professional.

Martin, R. C. (2003). Agile Software Development: Principles, Patterns, and Practices. Prentice Hall.

Freeman, S., & Pryce, N. (2009). Growing Object-Oriented Software, Guided by Tests. Addison-Wesley Professional.

Astels, D. (2003). Test-Driven Development: A Practical Guide. Prentice Hall.

Guerra, E., & Aniche, M. (2016). Test-Driven Development in Practice: Principles, Patterns, and Techniques. University of São Paulo.

Cormen, T. H., Leiserson, C. E., Rivest, R. L., & Stein, C. (2001). Introduction to Algorithms. MIT Press.

Fagerholm, F., & Münch, J. (2012). “Developer Experience: Concept and Definition,” International Journal of Software Engineering and Knowledge Engineering.

Harrold, M. J., & Souffa, M. L. (1988). “An Incremental Approach to Unit Testing During Maintenance,” Proceedings of the International Conference on Software Maintenance.

Rothermel, G., & Harrold, M. J. (1997). “A Safe, Efficient Regression Test Selection Technique,” ACM Transactions on Software Engineering and Methodology.

Kim, S., Zimmermann, T., & Nagappan, N. (2000). Regression Testing: Best Practices and Emerging Trends. IEEE Software.

Harrison, W. (2000). Single-Subject Experimental Designs in Software Engineering. Addison-Wesley.

Bach, J., & Schroeder, P. (2004). Pairwise Testing: A Best Practice That Isn’t. Proceedings of the STARWEST Conference.

Baresi, L., & Young, M. (2001). “Test Oracles,” ACM SIGSOFT Software Engineering Notes.

Galbraith, S. D. (2012). Las Vegas Algorithms: Foundations and Applications. Cambridge University Press.

Dunlop, C., & Basili, V. R. (1982). “A Study of Software Design Practices,” IEEE Transactions on Software Engineering.

Porto, M. D., & Quiles, M. G. (2014). Dynamic Graph Algorithms for Community Detection. Springer.

R Core Team (2015). R: A Language and Environment for Statistical Computing. R Foundation for Statistical Computing.

Fagerholm, F., & Münch, J. (2012). “Developer Experience: Concept and Definition,” International Journal of Software Engineering and Knowledge Engineering.

Yoo, S., & Harman, M. (2012). “Regression Testing Minimization, Selection and Prioritization: A Survey,” Software Testing, Verification and Reliability.

MacKinnon, T., Freeman, S., & Craig, P. (2001). Endo-testing: Unit testing with mock objects. Addison-Wesley.