Nesne Tabanlı Programlama 2

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

Emre Can Yılmaz

Ondokuz Mayıs Üniversitesi

2026

Giriş: Kodumuzun Doğru Çalıştığını Nasıl Anlarız?

  • Program yazmak işin sadece bir kısmıdır.
  • Yazdığımız kodun beklediğimiz gibi çalıştığını nasıl kontrol edeceğiz?
  • Koda yeni bir özellik eklediğimizde, fark etmeden başka bir yeri bozmuş olabilir miyiz?
  • Bir hata oluştuğunda, bunun kaynağını nasıl bulacağız?

Bu hafta:

  • Kodumuzu otomatik kontrol etme: Birim Test
  • Testleri kullanarak geliştirme yapma: TDD
  • Hataları bulma ve inceleme: Debugging

Birim Test Nedir? (Unit Testing)

  • Birim test, kodun küçük ve bağımsız parçalarını tek tek sınama işlemidir.
  • Bu parça çoğu zaman bir fonksiyon ya da metot olur.
  • Amaç, ilgili parçanın verilen girdiler altında beklenen sonucu üretip üretmediğini kontrol etmektir.

Neden kullanırız?

  • Hataları erken fark etmek için
  • Değişikliklerden sonra eski davranışın bozulup bozulmadığını görmek için
  • Daha düzenli ve modüler kod yazmak için
  • Kodun nasıl kullanılacağını örneklemek için

Python’da Test: unittest Modülü

  • unittest, Python’un standart test kütüphanesidir.
  • Testleri sınıflar içinde düzenlememize yardımcı olur.

Temel adımlar:

  1. unittest.TestCase sınıfından türeyen bir test sınıfı yazılır.
  2. İsmi test_ ile başlayan test metotları tanımlanır.
  3. Beklenen davranış, assertion metotları ile kontrol edilir.

unittest ile Basit Bir Örnek

Kodumuz (matematik.py):

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

Test Dosyası (test_matematik.py)

import unittest
from matematik import topla

class TestToplama(unittest.TestCase):
    def test_iki_pozitif(self):
        sonuc = topla(5, 3)
        self.assertEqual(sonuc, 8)

    def test_pozitif_negatif(self):
        sonuc = topla(3, -5)
        self.assertEqual(sonuc, -2)

Terminalden çalıştırma:

python -m unittest test_matematik.py

Assertion Nedir?

  • Testlerde yalnızca fonksiyonu çağırmak yetmez.
  • Sonucun doğru olup olmadığını da kontrol etmemiz gerekir.
  • Bu kontrolü yapan ifadelere assertion denir.

Bir assertion başarısız olursa test de başarısız olur.

Sık Kullanılan Assertion Örnekleri

  • assertEqual(a, b)a ile b eşit mi?
  • assertNotEqual(a, b)a ile b farklı mı?
  • assertTrue(x)x doğru mu?
  • assertFalse(x)x yanlış mı?
  • assertIsNone(x)x, None mı?
  • assertIsNotNone(x)x, None değil mi?
  • assertRaises(HataTipi, fonksiyon, argümanlar...) → Beklenen hata oluşuyor mu?

Alternatif Bir Araç: pytest

  • pytest, daha sade yazımıyla öne çıkan popüler bir test aracıdır.
  • Çoğu durumda unittest’e göre daha az kodla test yazılır.

Avantajları:

  • Test sınıfı yazmak çoğu zaman gerekmez
  • Doğrudan Python’un assert ifadesi kullanılır
  • Test dosyalarını otomatik bulur

Kurulum:

pip install pytest

Aynı Testi pytest ile Yazmak

from matematik import topla

def test_iki_pozitif():
    sonuc = topla(5, 3)
    assert sonuc == 8

def test_pozitif_negatif():
    sonuc = topla(3, -5)
    assert sonuc == -2

Terminalden çalıştırma:

pytest

Başarılı Test, Başarısız Test

  • Testlerin hepsi geçerse, yazdığımız örnek durumlar açısından kod beklendiği gibi davranıyor demektir.
  • Bir test başarısız olursa, hangi durumda sorun çıktığını daha hızlı görürüz.
  • Yani testler sadece hata bulmaz; hatanın hangi girdide ortaya çıktığını da gösterir.

Soru 1

Bir string içindeki boşluk sayısını döndüren bosluk_say(metin) fonksiyonunuz olsun.

Bu fonksiyon için aklınıza gelen iki farklı test durumu ne olur?

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

  • Test ettiğimiz kod bazen tek başına çalışmaz.
  • API, veritabanı, dosya sistemi veya başka servisler kullanabilir.
  • Böyle durumlarda gerçek bağımlılığı kullanmak yerine onun taklit bir sürümünü kullanabiliriz.

Buna mocking denir.

Neden kullanırız?

  • Testi yalnızca kendi kodumuza odaklamak için
  • Gerçek ağ ya da dosya işlemlerinin yavaşlığından kaçınmak için
  • Hata durumlarını kontrollü biçimde üretmek için

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"]

Mock ile Test Etme

# test_app.py
import unittest
from unittest.mock import patch, Mock
from app_code import get_kullanici_adi

class TestAPI(unittest.TestCase):
    @patch("app_code.requests.get")
    def test_kullanici_adi_getir(self, mock_get):
        mock_response = Mock()
        mock_response.json.return_value = {"name": "Ayşe"}
        mock_get.return_value = mock_response

        isim = get_kullanici_adi(5)

        self.assertEqual(isim, "Ayşe")

Bu örnekte asıl amaç, gerçek API’ye gitmeden fonksiyonun doğru değeri işleyip işlemediğini görmektir.

Ek Doğrulama: Doğru Çağrı Yapıldı mı?

mock_get.assert_called_once_with(
    "https://api.example.com/users/5"
)
  • Bu satır, yalnızca sonucun doğru olup olmadığını değil,
  • doğru URL ile çağrı yapılıp yapılmadığını da kontrol eder.

Yani burada hem çıktı hem de kullanım biçimi test edilmiş olur.

Soru 2

Bir fonksiyonun dosyaya log kaydı yazdığını düşünün.

Test sırasında gerçekten dosya oluşturmak istemiyorsanız, bu durumda hangi işlemi taklit etmek uygun olur?

  • open(...)
  • dosyanın write(...) işlemi
  • veya her ikisi

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

  • TDD, kodu yazmadan önce testi yazma yaklaşımıdır.
  • Yani önce “bu kod ne yapmalı?” sorusuna test ile cevap verilir.
  • Sonra testin geçmesi için gerekli en basit kod yazılır.

Amaç:

  • Gereksinimi netleştirmek
  • Gereğinden fazla kod yazmamak
  • Kodun test edilebilir kalmasını sağlamak

TDD Döngüsü: Red → Green → Refactor

  1. Red
    Önce başarısız olacak bir test yazılır.

  2. Green
    Testi geçirecek en basit kod yazılır.

  3. Refactor
    Kod iyileştirilir; testler hâlâ geçiyorsa devam edilir.

TDD Akışı

[ Test Yaz ]
     |
     v
[ Test Başarısız ]
     |
     v
[ En Basit Kodu Yaz ]
     |
     v
[ Test Başarılı ]
     |
     v
[ Kodu İyileştir ]
     |
     v
[ Yeni Test Ekle ]

TDD Örneği: Adım 1 — RED

İstek: is_pozitif(sayi) fonksiyonu yazılsın.

Önce testi yazarız:

# test_sayi_kontrol.py
from sayi_kontrol import is_pozitif

def test_pozitif_icin_true():
    assert is_pozitif(5) is True

Bu aşamada test başarısız olabilir; çünkü fonksiyon henüz yoktur ya da doğru yazılmamıştır.

TDD Örneği: Adım 2 — GREEN

Testi geçirecek en basit kod:

# sayi_kontrol.py
def is_pozitif(sayi):
    return True
  • Bu kod genel çözüm değildir.
  • Bu adımda amaç yalnızca ilk testi geçirmektir.
  • Bir sonraki test, bu çözümün neden yetersiz olduğunu gösterecektir.

TDD Örneği: Adım 3 — Yeni Test ve Düzeltme

Yeni bir test ekleyelim:

# test_sayi_kontrol.py
from sayi_kontrol import is_pozitif

def test_pozitif_icin_true():
    assert is_pozitif(5) is True

def test_negatif_icin_false():
    assert is_pozitif(-3) is False

Artık kodu düzeltmemiz gerekir:

# sayi_kontrol.py
def is_pozitif(sayi):
    return sayi > 0

Böylece her iki test de geçer.

Debugging Nedir?

  • Debugging, programdaki hatayı bulma ve düzeltme sürecidir.
  • Amaç yalnızca hatayı silmek değildir.
  • Asıl amaç, hatanın neden oluştuğunu anlamaktır.

Çünkü aynı hatanın benzeri daha sonra başka yerde tekrar ortaya çıkabilir.

IDE Debugger Kullanımı

VS Code, PyCharm gibi araçlardaki debugger’lar daha güçlüdür.

Temel özellikler:

  • Breakpoint: Kodun belirli bir satırda durmasını sağlar
  • Step Over / Step Into: Kodu adım adım ilerletir
  • Değişken inceleme: O anda değişkenlerin değerlerini gösterir

Bu yöntem, özellikle karmaşık hatalarda daha kullanışlıdır.

pdb ve breakpoint()

Python’da terminal tabanlı hata ayıklama için pdb kullanılabilir.

import pdb
pdb.set_trace()

Daha güncel ve kısa kullanım ise şudur:

breakpoint()
  • breakpoint(), uygun debugger’ı çağırır
  • Çoğu durumda pdb ile çalışır
  • Özellikle IDE dışı ortamlarda işe yarar

Sık Kullanılan pdb Komutları

  • n → bir sonraki satıra geç
  • s → fonksiyonun içine gir
  • p degisken → değişkenin değerini yazdır
  • c → programı devam ettir
  • q → debugger’dan çık

Bu komutlar, terminalde adım adım ilerleyerek hatayı incelemeyi sağlar.

Debugging Akışı

[ Beklenmedik Sonuç / Hata ]
           |
           v
[ Şüpheli Bölgeyi Belirle ]
           |
           v
[ Breakpoint Koy veya print/pdb Kullan ]
           |
           v
[ Kodu Adım Adım İncele ]
           |
           v
[ Hatanın Kaynağını Bul ]
           |
           v
[ Düzelt ve Yeniden Test Et ]

Soru 3

Kendi bilgisayarınızda bir IDE kullanarak, karmaşık bir hesaplama hatasını inceleyeceğinizi düşünün.

İlk olarak hangisini tercih etmek daha uygun olur?

    1. Çok sayıda print() eklemek
    1. IDE debugger ile breakpoint koyup adım adım ilerlemek
    1. Terminalde pdb kullanmak

Not: Yerel geliştirme ortamında çoğu zaman (B) daha rahattır.
Ancak IDE olmayan bir sunucu ya da terminal ortamında (C) daha uygun olabilir.

Terminoloji Özeti

  • Unit Test: Kodun küçük ve bağımsız bir parçasını test etme
  • Assertion: Beklenen davranışı kontrol eden ifade
  • Test Case: Bir grup testten oluşan yapı
  • Test Runner: Testleri bulan ve çalıştıran araç
  • Mocking: Gerçek bağımlılık yerine taklit nesne kullanma
  • Patching: Test sırasında bir nesnenin davranışını geçici olarak değiştirme
  • TDD: Önce test, sonra kod yaklaşımı
  • Debugging: Hata bulma ve inceleme süreci
  • Breakpoint: Debugger’ın duracağı nokta

Özet

  • Birim testler, kodun küçük parçalarını otomatik olarak kontrol eder.
  • unittest ve pytest, Python’da sık kullanılan test araçlarıdır.
  • Assertion’lar, testlerde ne beklediğimizi açık biçimde ifade eder.
  • Mocking, dış bağımlılıkları taklit ederek daha kontrollü test yazmamızı sağlar.
  • TDD, geliştirme sürecini test üzerinden yönlendirmeyi amaçlar.
  • Debugging ise hatayı yalnızca görmek değil, kaynağını anlamak için kullanılır.

Alıştırma 1

Bir metin_islemleri.py dosyasına ters_cevir(metin) fonksiyonunu yazın.

Ardından bu fonksiyon için test_metin.py dosyasında şu durumları test edin:

  • "python""nohtyp"
  • """"
  • "madam""madam"

Alıştırma 2

Aşağıdaki kod, listedeki en büyük sayıyı bulmaya çalışıyor. Ancak liste yalnızca negatif sayılardan oluştuğunda ve liste boş olduğunda sorun üretiyor.

Hatanın nerede olduğunu ve neden oluştuğunu bulun.

# hatali_kod.py
def en_buyuk_sayi(liste):
    en_buyuk = 0

    for sayi in liste:
        if sayi > en_buyuk:
            en_buyuk = sayi

    return en_buyuk

sonuc1 = en_buyuk_sayi([1, 5, 2, 8, 3])
sonuc2 = en_buyuk_sayi([-1, -5, -2, -8, -3])
sonuc3 = en_buyuk_sayi([])

print(f"Pozitif Liste Sonuç: {sonuc1}")
print(f"Negatif Liste Sonuç: {sonuc2}")
print(f"Boş Liste Sonuç: {sonuc3}")

İpucu: Başlangıç değeri ve boş liste durumu üzerinde düşünün.