Nesne Tabanlı Programlama 2

10 - Birim Test, Test Güdümlü Geliştirme (TDD) ve Debugging

Emre Can Yılmaz

Ondokuz Mayıs Üniversitesi

2025

Giriş: Kodumuz Çalışıyor mu? Emin miyiz?

  • Program yazmak işin sadece bir kısmı. Peki yazdığımız kodun doğru çalıştığından nasıl emin olacağız?
  • Ya bir değişiklik yaptığımızda, farkında olmadan başka bir yeri bozduysak?
  • Kodumuzda bir hata olduğunda bunu nasıl bulup düzelteceğiz?
  • Bu Hafta:
    • Kodumuzu otomatik kontrol etme yolları: Birim Test (Unit Testing)
    • Testleri kullanarak kod geliştirme: Test Güdümlü Geliştirme (TDD)
    • Hataları bulma sanatı: Hata Ayıklama (Debugging)

Birim Test Nedir? (Unit Testing)

  • Tanım: Kodumuzun en küçük, bağımsız parçalarını (genellikle fonksiyonlar veya metotlar) tek tek test etme işlemidir.
  • Amaç: Her bir “birimin” beklendiği gibi doğru çalışıp çalışmadığını kontrol etmek.
  • Neden Önemli?
    • Hataları Erken Bulma: Sorunları küçükken yakalamak daha kolaydır.
    • Güvenli Değişiklik: Kodu değiştirince eski testleri çalıştırıp bozulma olup olmadığını anlarız (Regression).
    • Daha İyi Tasarım: Test yazmak bizi daha modüler kod yazmaya iter.
    • Belgeleme: Testler, kodun nasıl kullanılacağını gösterir.

Python’da Test: unittest Modülü

  • Python’un standart test kütüphanesidir.
  • Testlerimizi sınıflar içinde organize eder.
  • Temel Adımlar:
    1. unittest.TestCase’den miras alan bir test sınıfı yaz.
    2. İsmi test_ ile başlayan test metotları tanımla.
    3. Metot içinde assertion (iddia) kullanarak beklentiyi kontrol et.

unittest ile Basit Toplama Testi

Kodumuz (matematik.py):

def topla(a, b):
  return a + b

Testimiz (test_matematik.py):

import unittest
from matematik import topla

class TestToplama(unittest.TestCase):
    def test_iki_pozitif(self):
        sonuc = topla(5, 3)
        # Vurgu: Beklentimizi kontrol ediyoruz
        self.assertEqual(sonuc, 8)

    def test_pozitif_negatif(self):
        sonuc = topla(10, -2)
        self.assertEqual(sonuc, 8)

Terminalden çalıştırma: python -m unittest test_matematik.py

Assertion’lar: İddialarımız

  • Assertion’lar, testlerimizin “kalbidir”. Kodumuzun ürettiği sonucun, beklediğimiz sonuçla aynı olup olmadığını kontrol ederler.
  • Eğer assertion başarısız olursa (beklenen sonuç ile gerçek sonuç farklıysa), test başarısız (FAIL) olur.

Assertion Örnekleri

  • unittest.TestCase sınıfında birçok assertion metodu bulunur:
    • assertEqual(a, b): a ile b eşit mi?
    • assertNotEqual(a, b): a ile b farklı mı?
    • assertTrue(x): x doğru (True) mu?
    • assertFalse(x): x yanlış (False) mu?
    • assertIsNone(x): x None mı?
    • assertIsNotNone(x): x None değil mi?
    • assertRaises(HataTipi, fonksiyon, argümanlar...): Fonksiyon çağrıldığında belirtilen hatayı fırlatıyor mu?

Alternatif: pytest Kütüphanesi

  • Daha modern ve genellikle daha basit bir test aracıdır. (pip install pytest)
  • Avantajları:
    • Daha az kod gerektirir (sınıf yazmak zorunlu değil).
    • Standart assert ifadesini kullanır.
    • Testleri otomatik bulur.

Aynı Testi pytest ile Yazmak

# test_matematik_pytest.py
from matematik import topla

def test_iki_pozitif():
    sonuc = topla(5, 3)
    # Vurgu: Standart assert kullanımı
    assert sonuc == 8 #[5]

def test_pozitif_negatif():
    sonuc = topla(10, -2)
    assert sonuc == 8 #[9]

# Terminalden çalıştırma: pytest

Çoğu zaman pytest daha okunaklı ve hızlıdır.

Soru 1: Ne Test Edersiniz? 🤔

Diyelim ki bir string’deki boşlukları sayan bosluk_say(metin) fonksiyonunuz var. Bu fonksiyon için aklınıza gelen 2 farklı test durumu ne olurdu? (Ne tür metinler gönderirdiniz?)

(Cevaplar: Boş metin ““, hiç boşluk olmayan”abc”, başta/sonda boşluk ” test “, normal”bu bir test” vb.)

Mocking: Bağımlılıkları Taklit Etme

  • Mocking Nedir? Test ettiğimiz kodun kullandığı dış dünyayı (API, veritabanı, dosya vb.) veya karmaşık nesneleri taklit etmektir.
  • Neden?
    • İzolasyon: Sadece kendi kodumuzu test etmek için.
    • Hız: Gerçek bağlantılar yavaştır.
    • Kontrol: Hata durumlarını simüle etmek için.
  • Nasıl Çalışır (Basitçe)?
    • Gerçek Nesne Çağrısı (örn: requests.get(...))
    • -> Yerine ->
    • Mock (Sahte) Nesne Çağrısı (Ayarladığımız sahte cevabı döndürür)

Mocking: API Testi Örneği (unittest.mock)

API’den veri çeken fonksiyon:

# app_code.py
import requests
def get_kullanici_adi(k_id):
    resp = requests.get(f"https://api.example.com/users/{k_id}")
    return resp.json()["name"]

Test (Gerçek API’ye bağlanmadan):

# test_app.py
import unittest
from unittest.mock import patch, Mock # Mock ve patch lazım
from app_code import get_kullanici_adi

class TestAPI(unittest.TestCase):
    @patch('app_code.requests.get') # requests.get'i taklit et
    def test_kullanici_adi_getir(self, mock_get): # mock_get taklit nesnesi
        # Taklit nesne nasıl davransın?
        mock_response = Mock()
        mock_response.json.return_value = {"name": "Ayşe"} # .json() -> {"name": "Ayşe"}
        mock_get.return_value = mock_response # get() -> mock_response

        isim = get_kullanici_adi(5) # Fonksiyonu çağır (mock çalışacak)

        mock_get.assert_called_once_with("https://api.example.com/users/5") # Doğru URL çağrıldı mı?
        self.assertEqual(isim, "Ayşe") # Doğru isim geldi mi?

@patch, requests.get yerine mock_get nesnesini kullanmamızı sağladı.

Soru 2: Neyi Mock’lardınız? 🤔

Bir dosyaya log kaydı yazan bir fonksiyonu test etmek istiyorsunuz. Testlerinizin gerçek bir dosya oluşturmasını istemiyorsunuz. Bu durumda neyi “mock” ederdiniz (taklit ederdiniz)?

(Cevap: Dosya açma (open) işlemini veya dosyaya yazma (write) metodunu)

Test Güdümlü Geliştirme (TDD)

  • Test-Driven Development (TDD): Kodu yazmadan önce o kodun ne yapması gerektiğini tanımlayan testi yazma yaklaşımıdır.
  • TDD Döngüsü (Red -> Green -> Refactor):
    1. RED -> Önce Başarısız bir test yaz (kod yok).
    2. GREEN -> Testi Başarılı yapacak en basit kodu yaz.
    3. REFACTOR -> Kodu İyileştir (testler hala başarılı).
  • Amaç: Gereksinimleri netleştirmek, sadece gerekli kodu yazmak ve kodun her zaman test edilebilir olmasını sağlamak.

TDD Akışı (Metinsel Görselleştirme)

+--------------------------+      +--------------------------+      +-----------------------+
| 1. Test Yaz (RED)        | ---> | 2. Kodu Yaz (GREEN)      | ---> | 3. Kodu İyileştir     |
| (Başarısız olacak        |      | (Testi geçecek en       |      |   (REFACTOR)          |
|  çünkü kod yok)          |      |  basit kod)              |      | (Testler hala yeşil)  |
+--------------------------+      +--------------------------+      +-----------------------+
          ^                                                                   |
          |--------------------<---- Yeni Test Ekle <-------------------------+

TDD: Çok Basit Örnek Adımları

İstek: is_pozitif(sayi) fonksiyonu yaz.

  1. RED: Testi yaz (test_pozitif.py):

    # from sayi_kontrol import is_pozitif # Yok!
    def test_pozitif_icin_true():
      # assert is_pozitif(5) == True # Hata verir
      pass
  1. GREEN: En basit kodu yaz (sayi_kontrol.py):

    def is_pozitif(sayi): return True # Testi geçer (ama yanlış)

    Testi tekrar yaz/çalıştır (test_pozitif.py):

    from sayi_kontrol import is_pozitif
    def test_pozitif_icin_true(): assert is_pozitif(5) == True # Geçer
  1. REFACTOR: Yeni test (negatif için) ekle (RED):

    # test_pozitif.py
    # ... önceki test ...
    def test_negatif_icin_false(): assert is_pozitif(-3) == False # Hata verir

    Kodu düzelt (GREEN & REFACTOR):

    # sayi_kontrol.py
    def is_pozitif(sayi): return sayi > 0 # Doğru mantık

Tüm testler şimdi geçer.

Hata Ayıklama (Debugging) Nedir?

  • Tanım: Programlardaki hataları (bug) bulma ve düzeltme sürecidir.
  • Neden Gerekli? Hatalar kaçınılmazdır. Sorunun kaynağını bulmak gerekir.
  • Amaç: Sadece hatayı düzeltmek değil, neden oluştuğunu anlamak.

Debugging Yöntemleri

  1. print() ile Debugging:
    • En temel yöntem. Kodun içine print(degisken) ekleyerek değerleri takip etmek.
    • İyi Yanı: Basit, hızlı.
    • Kötü Yanı: Kodu kirletir, karmaşık hatalarda yetersiz kalır.
    def hesapla(a, b):
      print(f"Hesapla girdi: a={a}, b={b}") # DEBUG
      sonuc = a / b # Hata burada olabilir mi?
      print(f"Sonuç={sonuc}") # DEBUG
      return sonuc
  1. IDE Debugger’ları (VS Code, PyCharm vb.):
    • Breakpoint: Kodu istediğin satırda durdurur.
    • Adım Adım İzleme: Kodu satır satır çalıştırır (Step Over/Into).
    • Değişken İzleme: Durduğu anda değişkenlerin değerlerini gösterir.
    • En Güçlü Yöntem: Kodu değiştirmeden derinlemesine analiz sağlar.
  1. Python Debugger (pdb):
    • Terminalde çalışan debugger. Koda import pdb; pdb.set_trace() eklenir.
    • IDE olmayan ortamlarda kullanışlıdır.

Debugging Akışı (Metinsel Görselleştirme)

[ Kod Çalışır ] ---> [ Beklenmedik Sonuç / Hata ]
      |
      V
[ Şüpheli Bölgeye Breakpoint Koy ]
      |
      V
[ Programı Debug Modunda Çalıştır ] ---> [ Kod Breakpoint'te Durur ]
      |                                          |
      V                                          V
[ Değişkenleri İncele ('Inspect') ] <--- [ Adım Adım İlerle ('Step') ] ---> [ Hatanın Kaynağını Bul ]

Soru 3: Hangi Yöntem? 🤔

Çok karmaşık bir matematiksel hesaplama yapan ve ara sıra yanlış sonuç veren bir fonksiyonunuz var. Hatayı bulmak için hangi debugging yöntemini öncelikle tercih ederdiniz? Neden?

    1. Bol bol print() eklemek
    1. IDE Debugger ile breakpoint koyup adım adım izlemek
    1. pdb kullanmak

(Cevap: Genellikle (B) en etkilidir, çünkü değişkenleri adım adım takip etmek ve kod akışını görmek karmaşık durumlarda daha kolaydır. print yetersiz kalabilir, pdb ise IDE kadar görsel olmayabilir.)

Özet: Kaliteli Kod İçin

  • Birim Testler: Kodun küçük parçalarını otomatik kontrol eder, güven verir. (unittest, pytest)
  • Assertion’lar: Testlerdeki beklentileri doğrular.
  • Mocking: Dış bağımlılıkları taklit ederek testleri izole eder. (unittest.mock)
  • TDD: Önce test yazarak daha sağlam kod geliştirmeyi hedefler. (Red->Green->Refactor)
  • Debugging: Hataları bulma ve anlama sürecidir (print, IDE Debugger, pdb).

Test yazmak ve hata ayıklamak, profesyonel yazılım geliştirmenin temel adımlarıdır!

Terminoloji Özeti

  • Unit Test (Birim Test): Kodun küçük, bağımsız bir parçasını test etme.
  • Assertion (İddia): Test içinde bir koşulun doğruluğunu kontrol etme ifadesi (assert a == b).
  • Test Case: unittest’te bir grup testi içeren sınıf.
  • Test Runner: Testleri bulan ve çalıştıran araç (unittest modülü, pytest komutu).
  • Mocking: Gerçek nesneleri taklit eden sahte nesneler kullanma.
  • Patching: Test sırasında bir nesnenin veya metodun davranışını geçici olarak değiştirme (genellikle mock ile).
  • TDD (Test-Driven Development): Önce test yazıp sonra kodu yazma yaklaşımı.
  • Debugging: Koddaki hataları bulma ve düzeltme süreci.
  • Breakpoint: Debugger’ın programı durduracağı kod satırı.

Alıştırmalar ✍️

  1. Basit Test Yazma (pytest veya unittest):
    • Bir metin_islemleri.py dosyasına, bir string’i ters çeviren ters_cevir(metin) fonksiyonu yazın.
    • Bu fonksiyon için aşağıdaki testleri içeren test_metin.py dosyası oluşturun:
      • Normal bir kelimeyi ters çeviriyor mu? ("python" -> "nohtyp")
      • Boş bir string’i doğru işliyor mu? ("" -> "")
      • Palindrom bir kelimeyi doğru işliyor mu? ("madam" -> "madam")
  1. Hata Ayıklama (print veya Debugger):
    • Aşağıdaki kod, bir listedeki en büyük sayıyı bulmayı amaçlıyor ama negatif sayılar olduğunda veya liste boş olduğunda hatalı çalışıyor.
    • liste = [-1, -5, -2, -8, -3] ve liste = [] durumlarında hatanın nerede olduğunu ve nedenini bulun.
    # hatali_kod.py
    def en_buyuk_sayi(liste):
        en_buyuk = None # Başlangıç değeri sorunlu olabilir
        for sayi in liste:
            # None ile karşılaştırma veya ilk elemanı atama mantığı eksik
            if en_buyuk is None or sayi > en_buyuk:
                en_buyuk = sayi
        return en_buyuk
    
    # Test Durumları
    # sonuc1 = en_buyuk_sayi([1, 5, 2, 8, 3]) # Bu doğru çalışır (8)
    sonuc2 = en_buyuk_sayi([-1, -5, -2, -8, -3]) # Ne döndürmeli? Ne döndürüyor?
    sonuc3 = en_buyuk_sayi([]) # Ne döndürmeli? Ne döndürüyor?
    print(f"Negatif Liste Sonuç: {sonuc2}")
    print(f"Boş Liste Sonuç: {sonuc3}")