← Kursa Dön
📄 Text · 18 min

Test Yazma: unittest ve pytest

Bir cambaz düşünün. Sahneye çıkıyor, ip üzerinde yürüyor, havada taklalar atıyor. Ama altında her zaman bir güvenlik ağı var. Düşerse ölmez, kalkar tekrar dener. İşte yazılımda testler tam olarak bu güvenlik ağıdır.

Kod yazıyorsun, çalışıyor, harika. Ama sonra bir değişiklik yapıyorsun ve bamm — eski bir şey bozuluyor. Testlerin varsa, bu bozulmayı anında fark edersin. Yoksa... müşteri fark eder. Ve bu hiç hoş olmaz.

Bu derste Python'da test yazmanın temellerini öğreneceğiz. unittest ile başlayıp, pytest'e geçeceğiz. Mocking, coverage ve TDD kavramlarına da göz atacağız.


Neden Test Yazarız?

"Kodumu çalıştırıyorum, çalışıyor işte. Neden ayrıca test yazayım ki?"

Bu soruyu her geliştirici hayatında en az bir kez sorar. Cevap basit: bugün çalışıyor olması, yarın da çalışacağı anlamına gelmez.

Testlerin Sağladığı Faydalar

  1. Güvenlik ağı: Yeni özellik eklerken eski kodun bozulup bozulmadığını anında görürsün

  2. Dokümantasyon: Testler, kodun nasıl kullanılacağını gösterir

  3. Cesaret: Refactoring yaparken "acaba bir şey bozulur mu?" korkusu olmaz

  4. Hız: Manuel test etmek saatler alır, otomatik testler saniyeler

Düşün ki bir e-ticaret sitesi yazıyorsun. Sepet modülünde bir değişiklik yaptın. Ödeme hâlâ çalışıyor mu? İndirim hesaplaması doğru mu? Kargo ücreti hâlâ ekleniyor mu? Bunları her seferinde elle kontrol etmek... işkence.

# Test olmadan hayat
def add_to_cart(cart, item):
    cart.append(item)
    return cart

# "Çalışıyor gibi" deyip geçmek...
result = add_to_cart([], "laptop")
print(result)  # ['laptop'] — tamam çalışıyor

# Ama ya edge case'ler?
# None geçsek? Aynı ürünü iki kez eklersek?
# Stokta yoksa? Fiyat negatifse?

Test yazmak "ekstra iş" değil, gelecekteki senin için yatırım. 6 ay sonra o koda döndüğünde, testler sana "her şey yolunda" ya da "burada bir sorun var" diyecek.


unittest — Python'ın Built-in Test Framework'ü

Python'la birlikte gelen unittest modülü, Java'nın JUnit'inden esinlenmiştir. Biraz verbose (uzun yazımlı) olsa da, standart kütüphanede olması büyük avantaj.

İlk Test Sınıfımız

import unittest

def multiply(a, b):
    return a * b

class TestMultiply(unittest.TestCase):
    def test_positive_numbers(self):
        self.assertEqual(multiply(3, 4), 12)
    
    def test_negative_numbers(self):
        self.assertEqual(multiply(-2, 5), -10)
    
    def test_zero(self):
        self.assertEqual(multiply(7, 0), 0)
    
    def test_floats(self):
        self.assertAlmostEqual(multiply(0.1, 0.2), 0.02)

if __name__ == "__main__":
    unittest.main()

Bunu test_multiply.py olarak kaydedip terminalde çalıştırıyorsun:

python -m unittest test_multiply.py

Çıktı:

....
----------------------------------------------------------------------
Ran 4 tests in 0.001s

OK

Her nokta bir başarılı test. 4 nokta = 4 test geçti. Hepsi OK!

Temel Assert Metodları

unittest.TestCase sınıfı bir sürü assert metodu sunar. En çok kullanılanlar:

import unittest

class TestAssertMethods(unittest.TestCase):
    def test_equality(self):
        self.assertEqual(1 + 1, 2)          # Eşit mi?
        self.assertNotEqual(1 + 1, 3)       # Eşit değil mi?
    
    def test_truthiness(self):
        self.assertTrue(10 > 5)              # True mu?
        self.assertFalse(10 < 5)             # False mu?
    
    def test_identity(self):
        self.assertIsNone(None)              # None mu?
        self.assertIsNotNone("hello")        # None değil mi?
    
    def test_membership(self):
        self.assertIn(3, [1, 2, 3])          # İçinde mi?
        self.assertNotIn(4, [1, 2, 3])       # İçinde değil mi?
    
    def test_types(self):
        self.assertIsInstance(42, int)        # Bu türde mi?
    
    def test_exceptions(self):
        with self.assertRaises(ValueError):  # Hata fırlatıyor mu?
            int("not_a_number")

💡 İpucu: assertEqual yerine düz assert de kullanabilirsin ama unittest'in assert metodları başarısız olduğunda çok daha açıklayıcı hata mesajı verir. assertEqual(result, 42) başarısız olursa "AssertionError: 15 != 42" der. Düz assert sadece "AssertionError" der.

setUp ve tearDown

Her testten önce ve sonra çalışacak hazırlık/temizlik kodları yazabilirsin:

import unittest
import os

class TestFileOperations(unittest.TestCase):
    def setUp(self):
        """Her testten ÖNCE çalışır"""
        self.test_file = "test_data.txt"
        with open(self.test_file, "w") as f:
            f.write("test content")
        print(f"  setUp: {self.test_file} oluşturuldu")
    
    def tearDown(self):
        """Her testten SONRA çalışır"""
        if os.path.exists(self.test_file):
            os.remove(self.test_file)
        print(f"  tearDown: {self.test_file} silindi")
    
    def test_file_exists(self):
        self.assertTrue(os.path.exists(self.test_file))
    
    def test_file_content(self):
        with open(self.test_file) as f:
            content = f.read()
        self.assertEqual(content, "test content")
    
    def test_file_size(self):
        size = os.path.getsize(self.test_file)
        self.assertGreater(size, 0)

setUp her test metodu için ayrı ayrı çalışır. Yani testler birbirinden bağımsız kalır. Bu çok önemli — testler arası bağımlılık, test yazmanın en büyük günahıdır.

Ayrıca setUpClass ve tearDownClass var. Bunlar tüm sınıf için bir kez çalışır:

class TestDatabase(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        """Tüm testlerden ÖNCE bir kez çalışır"""
        cls.db = create_test_database()
        print("Veritabanı oluşturuldu")
    
    @classmethod
    def tearDownClass(cls):
        """Tüm testlerden SONRA bir kez çalışır"""
        cls.db.destroy()
        print("Veritabanı silindi")
    
    def setUp(self):
        """Her testten önce"""
        self.db.begin_transaction()
    
    def tearDown(self):
        """Her testten sonra"""
        self.db.rollback()

Test Dosya Yapısı

Test dosyalarının bir düzeni olmalı. Genel kabul görmüş yapı:

my_project/
├── my_project/
│   ├── __init__.py
│   ├── calculator.py
│   └── utils.py
├── tests/
│   ├── __init__.py
│   ├── test_calculator.py
│   └── test_utils.py
└── setup.py

Kurallar basit:

  • Test dosyaları test_ ile başlar

  • Test sınıfları Test ile başlar

  • Test metodları test_ ile başlar

Bu isimlendirme kurallarına uyarsan, test runner'lar testlerini otomatik olarak bulur.


pytest — Modern Python Testing

unittest iş görür ama biraz uzun yazımlıdır. pytest ise Python dünyasının en popüler test framework'üdür. Daha az kod, daha okunabilir, daha güçlü.

Kurulum

pip install pytest

İlk pytest Testi

# test_basic.py
def add(a, b):
    return a + b

def test_add_positive():
    assert add(2, 3) == 5

def test_add_negative():
    assert add(-1, -1) == -2

def test_add_zero():
    assert add(5, 0) == 5

Fark ettin mi? Sınıf yok, self.assertEqual yok. Sadece düz assert. Bu kadar basit!

Çalıştırmak için:

pytest test_basic.py -v
test_basic.py::test_add_positive PASSED
test_basic.py::test_add_negative PASSED
test_basic.py::test_add_zero PASSED

========================= 3 passed in 0.02s =========================

-v (verbose) bayrağı her testin adını gösterir. Onsuz sadece nokta/F görürsün.

pytest'in Sihirli assert'ü

pytest'in en güzel özelliği: düz assert kullanırsın ama hata mesajları harika:

def test_list_comparison():
    expected = [1, 2, 3, 4, 5]
    actual = [1, 2, 3, 4, 6]
    assert actual == expected

Çıktı:

E       assert [1, 2, 3, 4, 6] == [1, 2, 3, 4, 5]
E         At index 4 diff: 6 != 5

pytest assert'i yeniden yazarak (rewriting) detaylı hata mesajı üretir. Bu unittest'in en büyük dezavantajlarından birini ortadan kaldırır.

Exception Test Etme

import pytest

def divide(a, b):
    if b == 0:
        raise ValueError("Cannot divide by zero!")
    return a / b

def test_divide_by_zero():
    with pytest.raises(ValueError) as exc_info:
        divide(10, 0)
    assert "Cannot divide by zero" in str(exc_info.value)

def test_divide_normal():
    assert divide(10, 2) == 5.0

Fixtures — Test Hazırlığının Zarif Yolu

unittest'te setUp/tearDown vardı. pytest'te fixture var. Çok daha esnek:

import pytest

@pytest.fixture
def sample_list():
    """Her test için taze bir liste döner"""
    return [1, 2, 3, 4, 5]

@pytest.fixture
def empty_dict():
    return {}

def test_list_length(sample_list):
    assert len(sample_list) == 5

def test_list_sum(sample_list):
    assert sum(sample_list) == 15

def test_dict_is_empty(empty_dict):
    assert len(empty_dict) == 0

Fixture'ı parametre olarak yazdığın an, pytest onu otomatik olarak çağırıp sonucunu sana verir. Sihir gibi!

Fixture'da Setup ve Teardown

yield kullanarak hem hazırlık hem temizlik yapabilirsin:

import pytest
import sqlite3

@pytest.fixture
def db_connection():
    # Setup kısmı
    conn = sqlite3.connect(":memory:")
    conn.execute("CREATE TABLE users (id INTEGER, name TEXT)")
    conn.execute("INSERT INTO users VALUES (1, 'Ali')")
    conn.commit()
    
    yield conn  # Testi bu bağlantıyla çalıştır
    
    # Teardown kısmı
    conn.close()

def test_user_count(db_connection):
    cursor = db_connection.execute("SELECT COUNT(*) FROM users")
    count = cursor.fetchone()[0]
    assert count == 1

def test_user_name(db_connection):
    cursor = db_connection.execute("SELECT name FROM users WHERE id=1")
    name = cursor.fetchone()[0]
    assert name == "Ali"

conftest.py — Paylaşılan Fixture'lar

Birden fazla test dosyasında aynı fixture'ı kullanmak istiyorsan, conftest.py dosyasına koy:

# tests/conftest.py
import pytest

@pytest.fixture
def api_client():
    """Tüm test dosyaları bu fixture'ı kullanabilir"""
    client = TestClient()
    client.login("test_user", "test_pass")
    yield client
    client.logout()

@pytest.fixture
def sample_user():
    return {
        "name": "Ahmet",
        "email": "ahmet@example.com",
        "age": 25
    }

conftest.py dosyasını import etmene gerek yok. pytest onu otomatik bulur ve içindeki fixture'ları tüm testlere sunar.

parametrize — Aynı Testi Farklı Verilerle Çalıştır

Bazen aynı testi farklı inputlarla çalıştırmak istersin. Kopyala-yapıştır yapma:

import pytest

def is_palindrome(s):
    s = s.lower().replace(" ", "")
    return s == s[::-1]

@pytest.mark.parametrize("text, expected", [
    ("racecar", True),
    ("hello", False),
    ("A man a plan a canal Panama", True),
    ("", True),
    ("ab", False),
    ("aba", True),
])
def test_palindrome(text, expected):
    assert is_palindrome(text) == expected

Çıktı:

test_palindrome[racecar-True] PASSED
test_palindrome[hello-False] PASSED
test_palindrome[A man a plan a canal Panama-True] PASSED
test_palindrome[-True] PASSED
test_palindrome[ab-False] PASSED
test_palindrome[aba-True] PASSED

Tek bir fonksiyonla 6 test! Her parametre kombinasyonu ayrı bir test olarak çalışır.


Test Türleri

Testler kapsamlarına göre üçe ayrılır. Bir piramit gibi düşün:

        /\
       /  \     E2E Tests (az ama kritik)
      /----\
     /      \   Integration Tests (orta)
    /--------\
   /          \ Unit Tests (çok ve hızlı)
  /____________\

Unit Test

Tek bir fonksiyon veya metodun doğru çalışıp çalışmadığını test eder. Dışa bağımlılık yok, çok hızlı çalışır.

def test_calculate_tax():
    assert calculate_tax(100, rate=0.18) == 18.0

def test_format_name():
    assert format_name("ahmet", "yılmaz") == "Ahmet Yılmaz"

Integration Test

Birden fazla parçanın birlikte çalışıp çalışmadığını test eder. Veritabanı, API gibi dış bağımlılıklar olabilir.

def test_user_registration(db_connection):
    user_service = UserService(db_connection)
    user = user_service.register("ali@example.com", "password123")
    
    # Veritabanına kayıt oldu mu?
    saved_user = user_service.get_by_email("ali@example.com")
    assert saved_user is not None
    assert saved_user.email == "ali@example.com"

E2E (End-to-End) Test

Tüm sistemi baştan sona test eder. Kullanıcı gibi davranır. Yavaş ama gerçekçi.

# Selenium veya Playwright ile
def test_login_flow(browser):
    browser.go_to("/login")
    browser.fill("email", "user@example.com")
    browser.fill("password", "secret123")
    browser.click("Login")
    assert browser.current_url == "/dashboard"
    assert browser.text_contains("Welcome back!")

⚠️ Dikkat: Test piramidini tersine çevirme! Çoğunluk unit test olmalı (hızlı, güvenilir). Integration testler orta düzeyde. E2E testler az ama kritik noktalar için. E2E testlere çok yaslanırsan, test suite'in yavaş ve kırılgan olur.


Mocking — Dış Bağımlılıkları Taklit Etme

Test yazarken bazen dış dünyaya bağımlılık olur: API çağrıları, veritabanı, dosya sistemi, zaman... Bunları gerçekten çağırmak istemezsin çünkü:

  • Yavaş olur

  • İnternet gerektirir

  • Yan etkileri olur

  • Sonuçlar değişken olabilir

İşte burada mock devreye girer. "Taklit nesne" oluşturursun.

unittest.mock ile Mocking

from unittest.mock import patch, MagicMock
import requests

def get_user_name(user_id):
    """API'den kullanıcı adını getirir"""
    response = requests.get(f"https://api.example.com/users/{user_id}")
    if response.status_code == 200:
        return response.json()["name"]
    return None

# Gerçek API'yi çağırmadan test et
@patch("requests.get")
def test_get_user_name(mock_get):
    # Mock'un ne döneceğini ayarla
    mock_response = MagicMock()
    mock_response.status_code = 200
    mock_response.json.return_value = {"name": "Ahmet", "id": 1}
    mock_get.return_value = mock_response
    
    # Fonksiyonu çağır
    result = get_user_name(1)
    
    # Doğrulamalar
    assert result == "Ahmet"
    mock_get.assert_called_once_with("https://api.example.com/users/1")

@patch dekoratörü requests.get'i bir mock ile değiştirir. Gerçek HTTP isteği yapılmaz.

MagicMock Detaylı

from unittest.mock import MagicMock

# MagicMock her şeyi kabul eder
mock = MagicMock()
mock.some_method(1, 2, 3)  # Hata yok
mock.any_attribute.nested.deep  # Hata yok

# Davranış tanımla
mock.calculate.return_value = 42
assert mock.calculate() == 42

# Side effect tanımla
mock.validate.side_effect = ValueError("Invalid!")
# mock.validate()  # ValueError fırlatır

# Çağrı kontrolü
mock.process("data")
mock.process.assert_called_once_with("data")

patch ile Context Manager

from unittest.mock import patch
from datetime import datetime

def get_greeting():
    hour = datetime.now().hour
    if hour < 12:
        return "Günaydın!"
    elif hour < 18:
        return "İyi günler!"
    return "İyi akşamlar!"

def test_morning_greeting():
    with patch("datetime.datetime") as mock_dt:
        mock_dt.now.return_value.hour = 9
        # Not: datetime mock'lamak biraz tricky olabilir
        # Gerçek projede freezegun kütüphanesi önerilir

def test_evening_greeting():
    with patch("datetime.datetime") as mock_dt:
        mock_dt.now.return_value.hour = 20

💡 İpucu: Çok fazla mock kullanmak, testlerin "gerçek" olmaktan çıkması demektir. Mock sayısı arttıkça testin güvenilirliği azalır. Eğer bir fonksiyonu test etmek için 5+ mock gerekiyorsa, belki fonksiyon çok fazla şey yapıyordur. Refactoring zamanı!


Coverage — Ne Kadar Kodun Test Edilmiş?

Test yazdın ama ne kadar kodun test edilmiş? İşte coverage (kapsam) bu soruya cevap verir.

pytest-cov Kurulumu

pip install pytest-cov

Kullanım

pytest --cov=my_project tests/

Çıktı:

---------- coverage: ... ----------
Name                    Stmts   Miss  Cover
-------------------------------------------
my_project/__init__.py      0      0   100%
my_project/calculator.py   20      4    80%
my_project/utils.py        15      8    47%
-------------------------------------------
TOTAL                      35     12    66%

HTML Rapor

pytest --cov=my_project --cov-report=html tests/

Bu komut htmlcov/ dizininde güzel bir HTML rapor oluşturur. Tarayıcıda açınca, hangi satırların test edilip hangilerinin edilmediğini satır satır görürsün. Yeşil satırlar test edilmiş, kırmızılar edilmemiş.

Hedef Coverage Oranı

pytest --cov=my_project --cov-fail-under=80 tests/

--cov-fail-under=80 sayesinde coverage %80'in altındaysa testler başarısız sayılır. CI/CD pipeline'ında bunu kullanmak, kaliteyi garanti altına alır.

# pytest.ini veya pyproject.toml ile de ayarlanabilir
# pyproject.toml
"""
[tool.pytest.ini_options]
addopts = "--cov=my_project --cov-fail-under=80"

[tool.coverage.run]
omit = ["tests/*", "setup.py"]
"""

⚠️ Dikkat: %100 coverage hedeflemek genellikle zaman kaybıdır. %80-90 arası iyi bir hedeftir. Önemli olan rakam değil, kritik iş mantığının test edilmiş olmasıdır. Getter/setter gibi trivial kodları test etmeye gerek yok.


TDD — Test Driven Development

TDD bir geliştirme yaklaşımıdır. Önce testi yaz, sonra kodu yaz. Üç adımda döner:

Red → Green → Refactor

  1. Red 🔴: Başarısız bir test yaz (çünkü daha kod yok)

  2. Green 🟢: Testi geçirecek minimum kodu yaz

  3. Refactor 🔵: Kodu iyileştir, testler hâlâ geçiyor mu kontrol et

# Adım 1: RED — Test yaz, başarısız olsun
def test_fizzbuzz():
    assert fizzbuzz(3) == "Fizz"
    assert fizzbuzz(5) == "Buzz"
    assert fizzbuzz(15) == "FizzBuzz"
    assert fizzbuzz(7) == "7"

# Bu noktada fizzbuzz fonksiyonu yok, test FAIL eder

# Adım 2: GREEN — Minimum kod yaz
def fizzbuzz(n):
    if n % 15 == 0:
        return "FizzBuzz"
    if n % 3 == 0:
        return "Fizz"
    if n % 5 == 0:
        return "Buzz"
    return str(n)

# Testler geçiyor!

# Adım 3: REFACTOR — İyileştir
def fizzbuzz(n):
    result = ""
    if n % 3 == 0:
        result += "Fizz"
    if n % 5 == 0:
        result += "Buzz"
    return result or str(n)

# Testler hâlâ geçiyor mu? Evet! Güzel.

TDD'yi her zaman uygulamak zorunda değilsin. Ama karmaşık iş mantığı yazarken, edge case'leri düşünmeni sağlar ve gereksiz kod yazmaktan korur.


Pratik: Calculator Test Suite

Şimdi öğrendiklerimizi birleştirelim. Bir Calculator sınıfı için kapsamlı bir test suite yazacağız.

Calculator Sınıfı

# calculator.py
class Calculator:
    def __init__(self):
        self.history = []
    
    def add(self, a, b):
        result = a + b
        self._record(f"{a} + {b} = {result}")
        return result
    
    def subtract(self, a, b):
        result = a - b
        self._record(f"{a} - {b} = {result}")
        return result
    
    def multiply(self, a, b):
        result = a * b
        self._record(f"{a} * {b} = {result}")
        return result
    
    def divide(self, a, b):
        if b == 0:
            raise ValueError("Cannot divide by zero!")
        result = a / b
        self._record(f"{a} / {b} = {result}")
        return result
    
    def _record(self, operation):
        self.history.append(operation)
    
    def get_history(self):
        return self.history.copy()
    
    def clear_history(self):
        self.history.clear()

Test Suite (pytest ile)

# test_calculator.py
import pytest
from calculator import Calculator

@pytest.fixture
def calc():
    """Her test için temiz bir Calculator instance'ı"""
    return Calculator()

# === Toplama Testleri ===
class TestAdd:
    def test_positive_numbers(self, calc):
        assert calc.add(2, 3) == 5
    
    def test_negative_numbers(self, calc):
        assert calc.add(-1, -1) == -2
    
    def test_mixed_numbers(self, calc):
        assert calc.add(-5, 3) == -2
    
    def test_floats(self, calc):
        result = calc.add(0.1, 0.2)
        assert abs(result - 0.3) < 1e-9
    
    def test_large_numbers(self, calc):
        assert calc.add(10**18, 10**18) == 2 * 10**18

# === Çıkarma Testleri ===
class TestSubtract:
    def test_basic(self, calc):
        assert calc.subtract(10, 3) == 7
    
    def test_negative_result(self, calc):
        assert calc.subtract(3, 10) == -7

# === Bölme Testleri ===
class TestDivide:
    def test_basic(self, calc):
        assert calc.divide(10, 2) == 5.0
    
    def test_float_result(self, calc):
        assert calc.divide(7, 2) == 3.5
    
    def test_divide_by_zero(self, calc):
        with pytest.raises(ValueError, match="Cannot divide by zero"):
            calc.divide(10, 0)

# === Çarpma Testleri ===
class TestMultiply:
    @pytest.mark.parametrize("a, b, expected", [
        (2, 3, 6),
        (0, 100, 0),
        (-2, 3, -6),
        (-2, -3, 6),
        (0.5, 4, 2.0),
    ])
    def test_multiply(self, calc, a, b, expected):
        assert calc.multiply(a, b) == expected

# === History Testleri ===
class TestHistory:
    def test_empty_history(self, calc):
        assert calc.get_history() == []
    
    def test_history_records(self, calc):
        calc.add(1, 2)
        calc.multiply(3, 4)
        history = calc.get_history()
        assert len(history) == 2
        assert "1 + 2 = 3" in history[0]
    
    def test_clear_history(self, calc):
        calc.add(1, 2)
        calc.clear_history()
        assert calc.get_history() == []
    
    def test_history_is_copy(self, calc):
        """get_history orijinal listeyi değil kopyasını döner"""
        calc.add(1, 2)
        history = calc.get_history()
        history.clear()  # Kopyayı temizle
        assert len(calc.get_history()) == 1  # Orijinal etkilenmez

Testleri Çalıştırma

# Tüm testleri çalıştır
pytest test_calculator.py -v

# Sadece bölme testlerini çalıştır
pytest test_calculator.py::TestDivide -v

# Belirli bir testi çalıştır
pytest test_calculator.py::TestAdd::test_positive_numbers -v

# Coverage ile çalıştır
pytest test_calculator.py --cov=calculator --cov-report=term-missing

--cov-report=term-missing eklersen, test edilmemiş satır numaralarını da görürsün:

Name            Stmts   Miss  Cover   Missing
----------------------------------------------
calculator.py      25      0   100%
----------------------------------------------

%100 coverage! 🎉


Bonus: pytest Marker'ları

Testleri kategorize etmek için marker kullanabilirsin:

import pytest

@pytest.mark.slow
def test_heavy_computation():
    """Bu test uzun sürer"""
    result = process_million_records()
    assert result > 0

@pytest.mark.integration
def test_database_connection():
    """Veritabanı gerektirir"""
    db = connect_to_db()
    assert db.is_connected()

@pytest.mark.skip(reason="Henüz implement edilmedi")
def test_future_feature():
    pass

@pytest.mark.skipif(
    sys.platform == "win32",
    reason="Windows'ta çalışmaz"
)
def test_unix_only():
    pass
# Sadece yavaş testleri çalıştır
pytest -m slow

# Yavaş testleri hariç tut
pytest -m "not slow"

# Integration testlerini çalıştır
pytest -m integration

Test Yazma Best Practices

  1. Arrange-Act-Assert (AAA) pattern'ini kullan:

def test_user_full_name():
    # Arrange — Hazırla
    user = User(first_name="Ahmet", last_name="Yılmaz")
    
    # Act — Çalıştır
    full_name = user.get_full_name()
    
    # Assert — Doğrula
    assert full_name == "Ahmet Yılmaz"
  1. Test adları açıklayıcı olsun: test_divide_by_zero_raises_value_error — ne test ettiği isimden anlaşılsın.

  2. Her test bir şeyi test etsin: Bir test metodunda 20 assert varsa, parçala.

  3. Testler birbirinden bağımsız olsun: Test A'nın sonucu Test B'yi etkilemesin.

  4. Edge case'leri unutma: Boş liste, None, negatif sayılar, çok büyük inputlar...

  5. Test de koddur: Temiz, okunabilir, bakımı kolay olsun.


Özet

  • Test yazmak kodun güvenlik ağıdır — değişiklik yaparken mevcut şeylerin bozulup bozulmadığını gösterir

  • unittest Python'ın built-in test framework'üdür: TestCase, assertEqual, setUp/tearDown

  • pytest daha modern ve basittir: düz assert, fixture'lar, parametrize

  • Mocking ile dış bağımlılıkları (API, veritabanı) taklit edebilirsin: patch, MagicMock

  • Coverage ile kodunun ne kadarının test edildiğini ölçersin, hedef %80+

  • TDD (Red → Green → Refactor) önce test yaz, sonra kod yaz yaklaşımıdır — iyi bir disiplindir