Principal Seviye Python Test Rehberi
Unit Test, Integration Test ve Testcontainers — Derinlemesine İnceleme
İçindekiler
- Test Felsefesi
- Test Piramidi ve Test Kupası
- Unit Test Temelleri
- pytest: Fiili Standart
- Fixture’lar Derinlemesine
- Parametrizasyon
- Test Çiftleri: Mock, Stub, Fake, Spy
- Property-Based Testing
- Integration Test
- Python için Testcontainers
- Test Mimarisi ve Proje Yapısı
- Coverage, Mutation Testing ve Test Kalitesi
- CI/CD Entegrasyonu
- Anti-Pattern’ler ve Yaygın Tuzaklar
- Principal Seviye Test Paketleri için Kontrol Listesi
1. Test Felsefesi
pytest veya unittest‘e dokunmadan önce, neden test yazdığımızı içselleştirmek gerekir. Principal seviye bir mühendis, bir coverage rozetini tatmin etmek için test yazmaz — testler şu amaçlarla var olur:
- Niyeti kodlamak. Bir test, çalıştırılabilir bir dokümantasyondur. Bir sonraki mühendise (çoğu zaman gelecekteki sen) kodun ne yapması gerektiğini söyler, sadece şu an ne yaptığını değil.
- Değişimi mümkün kılmak. Bir test paketinin asıl ekonomik değeri, korkmadan agresif bir şekilde refactor yapabilmenizi sağlamasıdır. Testi olmayan bir kod tabanı katılaşır — kimse dokunmaya cesaret edemez.
- Geri bildirim döngüsünü kısaltmak. Bir hatayı ne kadar erken bulursanız, düzeltmek o kadar ucuzdur. Bir unit test tarafından yakalanan bir hata saniyeler alır; aynı hata production’da yakalandığında olay müdahalesi, müşteri güveni ve muhtemelen para maliyeti getirir.
- Bir tasarım baskısı olarak işlev görmek. Test edilmesi zor kod genellikle kötü tasarlanmıştır — sıkı coupling, gizli bağımlılıklar, karışık sorumluluklar. Testi önce (ya da en azından erken) yazmak, bu kokuları katılaşmadan önce yüzeye çıkarır.
Temel Gerilim
Yazdığınız her test aynı zamanda bir yükümlülüktür: bakımı yapılmalı, flaky (kararsız) olabilir, CI’ı yavaşlatabilir ve yanlış bir güven duygusu verebilir. Test zanaatı “daha fazla test yaz” değildir — doğru seviyede, doğru izolasyonla, doğru maliyetle doğru testleri yazmaktır.
Bu, test piramidi fikriyle formalize edilir.
2. Test Piramidi ve Test Kupası
/\
/ \ E2E (az sayıda, yavaş, pahalı, yüksek güven)
/----\
/ \ Integration (orta miktarda, orta hız)
/--------\
/ \ Unit (çok sayıda, hızlı, ucuz, izole)
/------------\
- Unit testler: tek bir fonksiyonu/sınıfı/modülü izole olarak test eder. Milisaniyeler sürer. Yüzlerce, binlerce olabilir.
- Integration testler: kodunuz ile gerçek (veya gerçekçi) bir bağımlılık — veritabanı, mesaj kuyruğu, başka bir servis — arasındaki etkileşimi test eder. Saniyeler sürer. Onlarcadan yüzlerceye kadar olabilir.
- End-to-end (E2E) testler: tüm sistemi, gerçek arayüzleri (HTTP API, UI) üzerinden bir kara kutu olarak test eder. Dakikalar sürer. Bir avuç kadar.
Kent C. Dodds’un Test Kupası (Testing Trophy) fikri, özellikle API’ler ve veritabanları gibi zengin entegrasyon yüzeylerine sahip modern sistemler için faydalı bir rafinasyondur:
/‾‾‾‾‾‾‾‾‾‾‾‾\
| E2E |
\____________/
/‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾\
| Integration | <- paranın karşılığını en iyi veren katman
\________________/
/‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾\
| Unit |
\________________/
|
Statik Analiz
(mypy, ruff, flake8)
Kupa yaklaşımı, çoğu iş uygulaması için integration testlerin en iyi güven/maliyet oranını verdiğini savunur, çünkü gerçek hataların çoğu bileşenler arasındaki sınırlarda yaşar (serialization, SQL sorguları, network çağrıları) — iki sayıyı toplayan saf bir fonksiyonun içinde değil.
Principal seviye çıkarım: dogmatik bir şekilde 70/20/10 oranını kovalamayın. Bunun yerine her test için şunu sorun: “Bu hata sınıfını yakalayacak ve bana refactor için güven verecek en ucuz test hangisidir?”
3. Unit Test Temelleri
3.1 Bir Unit Testi Unit Test Yapan Nedir
Bir unit test şunları sağlamalıdır:
- Hızlı — milisaniyenin altından birkaç milisaniyeye kadar. 5.000 unit testten oluşan bir paket saniyeler içinde çalışmalı.
- İzole — network yok, disk I/O yok, gerçek veritabanı yok, sleep() yok. Sadece test edilen kod ve saf bellek içi işbirlikçilere bağlıdır.
- Deterministik — her seferinde, her makinede, herhangi bir sırada aynı girdi, aynı çıktı.
- Bağımsız — test sırası asla önemli olmamalıdır. Test A, test B’nin bağımlı olduğu bir durum bırakmamalıdır.
Eğer bir “unit test” gerçek bir Postgres örneğine erişiyorsa, bu bir unit test değildir — ne kadar küçük olursa olsun bir integration testtir.
3.2 AAA Deseni (Arrange–Act–Assert / Hazırla–Uygula–Doğrula)
def test_discount_applied_for_premium_customer():
# Arrange (Hazırla)
customer = Customer(tier="premium")
cart = Cart(items=[Item(price=100)])
# Act (Uygula)
total = calculate_total(cart, customer)
# Assert (Doğrula)
assert total == 90 # %10 indirim
Bu üç bölümü görsel olarak ayrı tutun — boş satırlar veya yorumlarla bile olsa. Kurulum ve doğrulamayı iç içe geçiren bir test okunması ve debug edilmesi daha zordur.
3.3 Test Başına Tek Davranış
Bir unit test, birden fazla assert ifadesi gerektirse bile tek bir mantıksal davranışı doğrulamalıdır (örneğin, aynı dönen nesnenin birden fazla özelliğini doğrulamak sorun değil; bir testte iki ilgisiz davranışı doğrulamak sorun).
# KÖTÜ: iki ilgisiz davranışı test ediyor
def test_user_creation_and_deletion():
user = create_user("alice")
assert user.id is not None
delete_user(user.id)
assert get_user(user.id) is None
# İYİ: ayrılmış
def test_create_user_assigns_id():
user = create_user("alice")
assert user.id is not None
def test_delete_user_removes_from_store():
user = create_user("alice")
delete_user(user.id)
assert get_user(user.id) is None
3.4 İsimlendirme Kuralları
Bir kural benimseyin ve zorunlu kılın. Güçlü bir örnek:
test_<test_edilen_birim>_<koşul>_<beklenen_sonuç>
def test_calculate_total_with_expired_coupon_raises_value_error(): ...
def test_parse_config_missing_required_key_returns_none(): ...
Bu isimlendirme, başarısız bir testin sadece adının (CI loglarında gösterildiği gibi) dosyayı açmadan neyin bozulduğunu size söylemesini sağlar.
4. pytest: Fiili Standart
unittest (standart kütüphane) sınıf tabanlıdır, XUnit tarzıdır, ayrıntılıdır ve Java tadındadır. pytest ise düz assert, fixture’lar ve geniş bir plugin ekosistemi sayesinde topluluk standardıdır. Principal seviye bir mühendis her ikisini de bilmelidir — çünkü pytest, unittest tarzı testleri değiştirmeden çalıştırabilir — ancak yeni kod için pytest deyimlerini varsayılan olarak kullanmalıdır.
4.1 İçgözlemli Düz Assertion’lar
def test_addition():
assert 2 + 2 == 4
self.assertEqual yok. pytest, import zamanında assert ifadesini yeniden yazar (AST rewriting yoluyla) ve zengin başarısızlık farkları (diff) verir:
E assert 5 == 4
E + where 5 = <function add at 0x...>(2, 3)
4.2 Exception (İstisna) Testi
import pytest
def test_division_by_zero_raises():
with pytest.raises(ZeroDivisionError):
1 / 0
def test_custom_error_message():
with pytest.raises(ValueError, match=r"invalid literal"):
int("not-a-number")
Exception tipi birden fazla hata koşulu için kullanıldığında, çıplak pytest.raises(ValueError) yerine regex ile match= kullanmayı tercih edin — aksi takdirde yanlış bir ValueError’ın fırlatıldığını doğruluyor olabilirsiniz.
4.3 Uyarılar (Warnings)
def test_deprecated_function_warns():
with pytest.deprecated_call():
old_function()
def test_specific_warning():
with pytest.warns(UserWarning, match="will be removed"):
risky_call()
4.4 Testleri İşaretleme ve Seçme
import pytest
@pytest.mark.slow
def test_expensive_computation():
...
@pytest.mark.skip(reason="henüz implemente edilmedi")
def test_future_feature():
...
@pytest.mark.skipif(sys.platform == "win32", reason="Sadece POSIX")
def test_file_permissions():
...
@pytest.mark.xfail(reason="bilinen hata, bkz. JIRA-123")
def test_known_broken_behavior():
...
Uyarıları önlemek ve pytest -m slow komutunu etkinleştirmek için özel marker’ları pyproject.toml‘da kayıt altına alın:
[tool.pytest.ini_options]
markers = [
"slow: yavaş testleri işaretler (-m \"not slow\" ile hariç tutulabilir)",
"integration: harici servis gerektiren integration testlerini işaretler",
]
Ardından CI’da hızlı geri bildirim döngüleri çalıştırabilirsiniz:
pytest -m "not slow and not integration" # sadece hızlı unit testler, pre-commit
pytest -m "integration" # integration paketi, ayrı bir CI aşaması
4.5 Konfigürasyon
pyproject.toml, modern, tek gerçek kaynaktır:
[tool.pytest.ini_options]
minversion = "7.0"
addopts = "-ra -q --strict-markers --strict-config"
testpaths = ["tests"]
python_files = "test_*.py"
python_classes = "Test*"
python_functions = "test_*"
filterwarnings = [
"error",
"ignore::DeprecationWarning:some_third_party_lib",
]
--strict-markers, kayıtsız bir@pytest.markkullanırsanız çalıştırmayı başarısız yapar; bu, yazım hatalarının sessizce hiçbir işe yaramamasını önler.filterwarnings = ["error"], uyarıları hatalara yükseltir — bu, deprecated API kullanımını, altı ay sonra kırıcı bir yükseltme haline gelmeden önce yakalar.
5. Fixture’lar Derinlemesine
Fixture’lar, pytest’in dependency-injection (bağımlılık enjeksiyonu) mekanizmasıdır ve scope/lifecycle kurallarını anlamak, orta seviye kullanımı ileri seviyeden ayırır.
5.1 Temel Fixture
import pytest
@pytest.fixture
def sample_user():
return User(name="Alice", email="[email protected]")
def test_user_email_domain(sample_user):
assert sample_user.email.endswith("@example.com")
pytest, sample_user‘ı testin parametre listesiyle isim eşleştirmesi yaparak çözer — bu “sihirli” ama deterministiktir.
5.2 Fixture Scope (Kapsam)
@pytest.fixture(scope="function") # varsayılan: her test için yeni bir örnek
@pytest.fixture(scope="class") # bir sınıftaki tüm testler arasında paylaşılır
@pytest.fixture(scope="module") # bir dosyadaki tüm testler arasında paylaşılır
@pytest.fixture(scope="package") # bir paket genelinde paylaşılır
@pytest.fixture(scope="session") # TÜM test çalıştırması boyunca paylaşılır
Temel kural: doğruluk için varsayılan olarak function scope kullanın (paylaşılan mutable durum yok = sıraya bağlı hata yok). Scope’u yalnızca pahalı, salt-okunur veya doğal olarak paylaşılan kaynaklar için session‘a genişletin — örneğin bir Testcontainers Postgres örneği başlatmak, büyük bir fixture veri setini derlemek veya bir ML modelini belleğe yüklemek.
5.3 yield ile Setup/Teardown
@pytest.fixture
def db_connection():
conn = create_connection()
yield conn # <- test burada çalışır
conn.close() # <- teardown, test başarısız olsa bile çalışır
yield‘den sonraki kod, test bir exception fırlatsa bile teardown sırasında çalışır, çünkü pytest fixture yürütmesini temizliği garanti edecek şekilde sarar — bu, dosya handle’ları, DB bağlantıları ve geçici dizinler gibi kaynaklar için kritiktir.
5.4 Fixture Kompozisyonu (Fixture’lara bağımlı fixture’lar)
@pytest.fixture
def db_connection():
conn = create_connection()
yield conn
conn.close()
@pytest.fixture
def user_repository(db_connection):
return UserRepository(db_connection)
@pytest.fixture
def sample_user(user_repository):
user = user_repository.create(name="Alice")
yield user
user_repository.delete(user.id)
Bu bir bağımlılık grafiği oluşturur, tembel (lazy) olarak çözülür ve scope başına önbelleğe alınır. Bu, xUnit setUp/tearDown‘dan gerçekten daha güçlüdür, çünkü fixture’lar birleştirilebilir ve test dosyaları arasında bağımsız olarak yeniden kullanılabilir.
5.5 autouse Fixture’lar
@pytest.fixture(autouse=True)
def reset_global_state():
yield
GlobalRegistry.clear()
Ölçülü kullanın. autouse fixture’lar, açıkça talep edilmeden kendi scope’larındaki her test için çalışır — invariantları zorunlu kılmak için güçlüdür (örn. “hiçbir test global durumu kirli bırakamaz”), ancak test davranışını çağrı noktasında daha az görünür/açık hale getirirler. Aşırı kullanım, bir testin üç dosya ötedeki bir autouse fixture yüzünden başarısız olduğu “uzaktan ürkütücü etki” durumuna yol açar.
5.6 conftest.py: Import Olmadan Fixture Paylaşımı
tests/
├── conftest.py # tests/ ve altındaki TÜM testler için görünür fixture'lar
├── unit/
│ ├── conftest.py # sadece tests/unit/ için görünür fixture'lar
│ └── test_pricing.py
└── integration/
├── conftest.py
└── test_repository.py
pytest, conftest.py dosyalarını otomatik olarak keşfeder — import gerekmez. Bu, çapraz kesen fixture’lar (DB bağlantıları, test client’ları, factory’ler) için doğru yerdir. Sadece unit fixture’ları tests/unit/conftest.py‘de, sadece integration fixture’ları (örn. Testcontainers) tests/integration/conftest.py‘de tutun, böylece unit test çalıştırmaları Docker’a bağımlı kodu asla import bile etmez.
5.7 Fixture Sonlandırma Sırası ve request
@pytest.fixture
def temp_dir(request):
d = Path(tempfile.mkdtemp())
def cleanup():
shutil.rmtree(d)
request.addfinalizer(cleanup)
return d
request.addfinalizer, koşullu olarak birden fazla finalizer kaydetmeniz gerektiğinde veya request.node (test öğesi), request.param (parametrize/indirect fixture’larda) veya request.config‘e (CLI seçenekleri) erişmeniz gerektiğinde yield teardown’a bir alternatiftir.
6. Parametrizasyon
6.1 Temel Parametrize
import pytest
@pytest.mark.parametrize("input_value,expected", [
(0, "zero"),
(1, "one"),
(-1, "negative"),
(100, "positive"),
])
def test_classify_number(input_value, expected):
assert classify_number(input_value) == expected
Bu, 4 bağımsız test senaryosu oluşturur — her biri test raporlarında ayrı ayrı gösterilir (test_classify_number[0-zero] vb.), böylece tek bir başarısızlık diğer üçünü gizlemez.
6.2 Okunabilirlik için Parametrize ID’leri
@pytest.mark.parametrize(
"raw,expected",
[
pytest.param("", None, id="empty_string"),
pytest.param(" ", None, id="whitespace_only"),
pytest.param("42", 42, id="valid_integer"),
pytest.param("abc", None, id="non_numeric"),
],
)
def test_parse_optional_int(raw, expected):
assert parse_optional_int(raw) == expected
Açık id= değerleri, pytest’in otomatik oluşturduğu ID’lerden (karmaşık nesneler için okunmaz olabilir) daha iyidir ve kendi kendini belgeleyen CI çıktısı verir.
6.3 Parametrize Yığınlama (Kartezyen Çarpım)
@pytest.mark.parametrize("currency", ["USD", "EUR", "GBP"])
@pytest.mark.parametrize("tier", ["free", "premium"])
def test_pricing_matrix(tier, currency):
price = get_price(tier=tier, currency=currency)
assert price > 0
İki parametrize dekoratörünü yığınlamak, tam Kartezyen çarpımı üretir (2 × 3 = 6 senaryo) — güçlüdür ama kombinatoryal olarak patlayabilir. Yalnızca bazı kombinasyonlar geçerliyse, yığınlama yerine açık tuple’larla pytest.mark.parametrize kullanın.
6.4 Fixture Parametrizasyonu (indirect=True)
@pytest.fixture
def api_client(request):
version = request.param
return APIClient(version=version)
@pytest.mark.parametrize("api_client", ["v1", "v2"], indirect=True)
def test_endpoint_across_versions(api_client):
assert api_client.get("/health").status_code == 200
indirect=True, parametrize değerini doğrudan test fonksiyonuna değil, (request.param olarak) fixture üzerinden yönlendirir — parametrenin ham bir değer değil kurulum mantığı gerektirdiği durumlarda kullanışlıdır.
7. Test Çiftleri: Mock, Stub, Fake, Spy
Gerard Meszaros’un (xUnit Test Patterns kitabından) taksonomisi kesindir ve içselleştirilmeye değer — “mock” terimi genellikle bunların hepsi için gelişigüzel kullanılır:
| Çift | Amaç | Örnek |
|---|---|---|
| Dummy | Geçirilir ama hiç kullanılmaz, sadece bir parametreyi doldurur | Kullanılmayan bir logger argümanı olarak geçirilen None |
| Stub | Çağrılara hazır cevaplar döndürür | Her zaman {"status": "success"} döndüren bir ödeme ağ geçidi stub’ı |
| Spy | Nasıl çağrıldığını sonraki doğrulama için kaydeder | send_email‘in belirli bir argümanla tam olarak bir kez çağrıldığını doğrulamak |
| Mock | Beklentilerle önceden programlanmış; beklentiler karşılanmazsa test başarısız olur | Bir etkileşimin belirli bir sırada gerçekleştiğini doğrulamak |
| Fake | Çalışan, basitleştirilmiş bir implementasyon | Postgres yerine geçen bellek içi bir SQLite DB; bellek içi dict tabanlı bir repository |
7.1 unittest.mock Temelleri
from unittest.mock import Mock, MagicMock, patch
def test_notify_calls_email_service():
email_service = Mock()
notifier = Notifier(email_service=email_service)
notifier.notify(user_id=1, message="Hello")
email_service.send.assert_called_once_with(user_id=1, message="Hello")
7.2 patch — Gerçek Bağımlılıkların Değiştirilmesi
from unittest.mock import patch
@patch("myapp.services.payment.requests.post")
def test_charge_card_success(mock_post):
mock_post.return_value.status_code = 200
mock_post.return_value.json.return_value = {"status": "approved"}
result = charge_card(card_number="4111111111111111", amount=100)
assert result.approved is True
mock_post.assert_called_once()
Kritik kural: patch’i tanımlandığı yerde değil, kullanıldığı yerde uygulayın. Eğer payment.py şunu yapıyorsa from requests import post, requests.post‘u değil myapp.services.payment.post‘u patch’lemeniz gerekir — çünkü isim bağlama import zamanında hedef modülün namespace’inde gerçekleşir.
7.3 Context Manager Olarak patch
def test_current_time_dependent_logic():
with patch("myapp.services.clock.datetime") as mock_datetime:
mock_datetime.now.return_value = datetime(2024, 1, 1)
result = is_new_year(datetime_provider=mock_datetime)
assert result is True
Patch’e testin sadece bir kısmı için ihtiyacınız olduğunda veya birçok patch’i yığınlamak dekoratör imzasını okunmaz hale getirdiğinde, dekoratörler yerine context manager’ları tercih edin.
7.4 autospec — Arayüz Kaymasını Önlemek
from unittest.mock import patch
@patch("myapp.services.payment.PaymentGateway", autospec=True)
def test_gateway_called_correctly(mock_gateway_cls):
mock_gateway_cls.return_value.charge.assert_not_called()
autospec=True olmadan, bir Mock seve seve mock.chrage(...) (bir yazım hatası) veya yanlış sayıda argümanı kabul eder — gerçek kod çökse bile test geçer. autospec=True (veya create_autospec), gerçek nesnenin imzasını içgözlemler ve eşleşmeyen çağrılarda TypeError fırlatır, gerçek sınıf değiştiğinde ama test çifti değişmediğinde arayüz kaymasını yakalar.
Principal seviye kural: en basit mock dışında her şey için her zaman autospec=True tercih edin.
7.5 Side Effect’ler
mock_repo = Mock()
mock_repo.get.side_effect = [User(id=1), User(id=2), KeyError("not found")]
# Ya da dinamik davranış için çağrılabilir bir side_effect
def fake_get(user_id):
if user_id not in db:
raise KeyError(user_id)
return db[user_id]
mock_repo.get.side_effect = fake_get
7.6 Mock Yerine Fake — Bir Tasarım Tercihi
Fake’ler, mimari sınırlarda genellikle mock’lara tercih edilir, çünkü:
- Gerçek mantığı çalıştırırlar (bellek içi bir repository yine de
get,save,delete‘i doğru şekilde implemente etmek zorundadır). - Testinizi implementasyon detaylarına bağlamazlar (mock’lar bir işbirlikçinin nasıl çağrıldığını doğrular; fake’ler ne olduğunu doğrular).
- Dahili çağrı desenini refactor etmek (örn.
save()‘i bir kez yerine iki kez çağırmak veya bir önbellek katmanı eklemek), bir fake kullanan testleri bozmaz, ancak çağrı sayılarını doğrulayan kırılgan mock tabanlı testleri bozar.
class InMemoryUserRepository:
def __init__(self):
self._users: dict[int, User] = {}
self._next_id = 1
def save(self, user: User) -> User:
user.id = self._next_id
self._users[user.id] = user
self._next_id += 1
return user
def get(self, user_id: int) -> User | None:
return self._users.get(user_id)
def test_user_service_creates_user():
repo = InMemoryUserRepository()
service = UserService(repo)
user = service.register("[email protected]")
assert repo.get(user.id).email == "[email protected]"
Bu test, gözlemlenebilir sözleşme korunduğu sürece UserService.register‘ın dahili yapısının yeniden yazılmasına dayanıklıdır — bu, iyi bir testin ayırt edici özelliğidir.
7.7 Mock’ların Doğru Araç Olduğu Durumlar
Etkileşimin kendisi test edilen davranış olduğunda katı mock/spy’lar kullanın:
- Bir olayın bir mesaj yoluna yayınlandığını doğrulamak.
- Üçüncü taraf bir API’nin tam doğru payload ile çağrıldığını doğrulamak (gerçekten çağırmadan).
- Retry/backoff mantığının bir bağımlılığı N kez çağırdığını doğrulamak.
8. Property-Based Testing
Örnek tabanlı testler (assert add(2, 3) == 5) sadece düşündüğünüz örnekleri doğrular. Hypothesis, hayal etmediğiniz uç durumları bulmak için yüzlerce düşmanca girdi üretir.
from hypothesis import given, strategies as st
@given(st.integers(), st.integers())
def test_addition_is_commutative(a, b):
assert add(a, b) == add(b, a)
@given(st.lists(st.integers()))
def test_sorted_list_is_ordered(lst):
result = my_sort(lst)
assert all(result[i] <= result[i+1] for i in range(len(result) - 1))
assert sorted(result) == sorted(lst) # aynı elemanlar, sadece sıralı değil
Hypothesis, başarısız örnekleri otomatik olarak küçültür (shrink) ve en küçük yeniden üretilebilir senaryoyu bulur — eğer [93, -4, 0, 17, -4] başarısız olursa, minimal başarısız girdiyi rapor eder, örn. [0, -1], bu da önemli miktarda debug süresinden tasarruf sağlar.
Property-based testing’i şunlar için kullanın:
- Cebirsel özelliklere sahip saf fonksiyonlar (değişmelik, idempotans, serialization round-trip).
- Parser’lar ve serializer’lar (
parse(serialize(x)) == x). - Büyük, sayılması zor bir girdi uzayına sahip herhangi bir şey.
9. Integration Test
9.1 Tanım ve Kapsam
Bir integration test, iki veya daha fazla bileşenin birlikte doğru çalıştığını doğrular, özellikle tam olarak kontrol etmediğiniz bir sınır boyunca: bir veritabanı, bir önbellek, bir mesaj aracısı, üçüncü taraf bir HTTP API veya dosya sistemi.
Unit testlerden temel fark: integration testlerin daha yavaş olmasına, gerçek altyapıya dokunmasına ve paylaşılan, pahalı kuruluma sahip olmasına izin verilir (beklenir) — ancak yine de deterministik ve test çalıştırmaları arasında izole olmalıdırlar.
9.2 Tarihsel Sorun: Veritabanını Mock’lamak
Yaygın bir anti-pattern, “integration” testlerinde veritabanını/ORM’yi tamamen mock’lamaktır:
# Bu GERÇEK bir integration test DEĞİLDİR — SQL doğruluğu hakkında hiçbir şey test etmez
@patch("myapp.repository.db.session")
def test_get_user(mock_session):
mock_session.query.return_value.filter.return_value.first.return_value = User(id=1)
...
Bu tür bir test yanlış bir güven verir: gerçek SQL sorgunuz hatalıysa, migration’ınız bozuksa veya ORM eşlemenizde bir yazım hatası varsa bile geçer. Gerçek hatalar SQL’de, connection pooling’de, transaction sınırlarında, JSON’a/dan serialization’da meydana gelir — tam olarak bu testin çalıştırmadığı kısımlarda.
9.3 Tarihsel Alternatifler (ve Sorunları)
- Postgres yerine bellek içi SQLite — hızlıdır, ancak SQLite’ın SQL diyalekti, tip sistemi ve kısıtlama uygulaması Postgres’ten farklıdır (örn. gerçek
JSONByok, farklıON CONFLICTsemantiği, window fonksiyon farklılıkları, daha zayıf tip kontrolü). Testler yerel olarak geçer, production’da başarısız olur. - Paylaşılan, uzun ömürlü bir test veritabanı — test çalıştırmaları arasında durum sızıntısı, birbirine giren paralel CI işleri ve her yere uygulanmayan şema değişikliklerinden kaynaklanan “benim makinemde çalışıyor” kaymaları nedeniyle kararsızdır.
- Testleri çalıştırmadan önce manuel olarak başlatılan Docker Compose — çalışır, ancak test yaşam döngüsüyle entegre değildir: container’ın hazır olduğunu garanti eden hiçbir şey yoktur, otomatik olarak kaldıran hiçbir şey yoktur ve paralel test yürütmesi veya CI matrix işleriyle iyi birleşmez.
Testcontainers her üç sorunu da çözer, gerçek, geçici, izole altyapıyı doğrudan test paketinin kendisinden, uygun hazır olma kontrolleri ve otomatik temizlik ile programatik olarak başlatarak.
10. Python için Testcontainers
10.1 Nedir
Testcontainers, test kodundan doğrudan, otomatik yaşam döngüsü yönetimi ile gerçek bağımlılıkları tek kullanımlık Docker container’ları olarak başlatmanızı sağlayan bir kütüphanedir (başlangıçta Java, şimdi Python, Go, .NET, Node vb. için mevcuttur).
pip install testcontainers[postgres]
10.2 Temel Postgres Örneği
import pytest
from testcontainers.postgres import PostgresContainer
import sqlalchemy
@pytest.fixture(scope="session")
def postgres_container():
with PostgresContainer("postgres:16-alpine") as postgres:
yield postgres
@pytest.fixture(scope="session")
def db_engine(postgres_container):
engine = sqlalchemy.create_engine(postgres_container.get_connection_url())
run_migrations(engine) # Alembic migration'larını uygula, vb.
return engine
@pytest.fixture
def db_session(db_engine):
connection = db_engine.connect()
transaction = connection.begin()
Session = sqlalchemy.orm.sessionmaker(bind=connection)
session = Session()
yield session
session.close()
transaction.rollback() # <- her testin temiz başlaması için geri al
connection.close()
def test_user_repository_saves_and_retrieves(db_session):
repo = UserRepository(db_session)
user = repo.save(User(email="[email protected]"))
fetched = repo.get(user.id)
assert fetched.email == "[email protected]"
Buradaki temel tasarım deseni: container’ın kendisi session scope’ludur (bir kez başlatılır, pahalıdır — genellikle 1-3 saniye), ancak her bir test, test bittikten sonra geri alınan (rollback) yeni bir transaction alır (db_session fixture’ı, function scope’lu). Bu size unit test benzeri izolasyon ve hız ile gerçek Postgres semantiği verir (transaction rollback neredeyse anlıktır, tabloları truncate etmekten veya her test için container’ı yeniden başlatmaktan çok daha hızlıdır).
10.3 Hazır Olma / Bekleme Stratejileri
Testcontainers, iyi bilinen image’lar için (Postgres, MySQL, Kafka, Redis’in hepsinin dahili bekleme stratejileri vardır, gerçek port/protokolü poll ederler) klasik “container başladı ama servis henüz hazır değil” yarış durumunu dahili olarak halleder. Özel image’lar için:
from testcontainers.core.container import DockerContainer
from testcontainers.core.waiting_utils import wait_for_logs
class MyServiceContainer(DockerContainer):
def __init__(self):
super().__init__("myregistry/myservice:latest")
self.with_exposed_ports(8080)
def start(self):
super().start()
wait_for_logs(self, "Server started on port 8080")
return self
“Container’ı beklemek” için asla time.sleep(2)‘ye güvenmeyin — bu hem yavaştır (her zaman tam sleep süresini öder) hem de kararsızdır (bazen yüklü bir CI runner’da yeterince uzun olmayabilir). Her zaman protokol seviyesinde veya log tabanlı hazır olma kontrolleri kullanın.
10.4 Birden Fazla Container (Ağ Üzerinden Bağlı)
from testcontainers.core.network import Network
from testcontainers.postgres import PostgresContainer
from testcontainers.redis import RedisContainer
@pytest.fixture(scope="session")
def app_stack():
with Network() as network:
postgres = PostgresContainer("postgres:16-alpine").with_network(network)
redis = RedisContainer("redis:7-alpine").with_network(network)
with postgres, redis:
yield {"postgres": postgres, "redis": redis}
10.5 Kafka Integration Test Örneği
from testcontainers.kafka import KafkaContainer
from confluent_kafka import Producer, Consumer
@pytest.fixture(scope="session")
def kafka_container():
with KafkaContainer("confluentinc/cp-kafka:7.5.0") as kafka:
yield kafka
def test_order_created_event_is_published(kafka_container):
bootstrap_servers = kafka_container.get_bootstrap_server()
producer_config = {"bootstrap.servers": bootstrap_servers}
consumer = Consumer({
"bootstrap.servers": bootstrap_servers,
"group.id": "test-group",
"auto.offset.reset": "earliest",
})
consumer.subscribe(["orders"])
service = OrderService(kafka_config=producer_config)
service.create_order(order_id="123", amount=100)
msg = consumer.poll(timeout=10)
assert msg is not None
assert json.loads(msg.value())["order_id"] == "123"
10.6 Özel Modülü Olmayan Herhangi Bir Şey için Generic Container
from testcontainers.core.container import DockerContainer
def test_against_custom_service():
with DockerContainer("myorg/legacy-soap-service:2.1") \
.with_exposed_ports(8080) \
.with_env("LICENSE_KEY", "test-key") as container:
port = container.get_exposed_port(8080)
host = container.get_container_host_ip()
response = requests.get(f"http://{host}:{port}/health")
assert response.status_code == 200
10.7 Testcontainers Modül Kapsamı (ekosistem olgunluğu itibarıyla)
Yaygın birinci sınıf modüller: postgres, mysql, mongodb, redis, kafka, rabbitmq, elasticsearch, localstack (AWS emülasyonu — S3, SQS, DynamoDB vb.), neo4j, clickhouse, mssql, nginx, selenium (tarayıcı tabanlı E2E için) ve k3s/kind (geçici Kubernetes cluster’ları).
localstack, cloud-native takımlar için özellikle önemlidir — gerçek AWS’ye dokunmadan, maliyet olmadan ve CI’da network erişimine gerek kalmadan, gerçek, geçici, yerel bir AWS emülasyonuna karşı AWS SDK (boto3) etkileşimlerini (S3 yüklemeleri, SQS mesaj akışları, DynamoDB sorguları) integration test etmenizi sağlar.
from testcontainers.localstack import LocalStackContainer
import boto3
@pytest.fixture(scope="session")
def localstack():
with LocalStackContainer("localstack/localstack:3") as ls:
ls.with_services("s3", "sqs")
yield ls
@pytest.fixture
def s3_client(localstack):
return boto3.client("s3", endpoint_url=localstack.get_url())
def test_upload_report_to_s3(s3_client):
s3_client.create_bucket(Bucket="reports")
upload_report(s3_client, bucket="reports", key="q1.csv", data=b"...")
obj = s3_client.get_object(Bucket="reports", Key="q1.csv")
assert obj["Body"].read() == b"..."
10.8 CI’da CPU/Kaynak Disiplini
Testcontainers, CI runner’ında bir Docker daemon’ı gerektirir. Dikkat edilmesi gerekenler:
- GitHub Actions / GitLab CI: çoğu barındırılan runner’da varsayılan olarak Docker-in-Docker veya bir Docker socket’i bulunur; pipeline’ınızda
docker info‘nun çalıştığını doğrulayın, bunu varsaymayın. - Kaynak sınırları: bir test paketinin kaçmasının CI runner’ını aç bırakmasını önlemek için container kaynaklarını sınırlayın (
.with_kwargs(mem_limit="512m")). - Reaper container: Testcontainers, test süreci öldürülse bile (örn. OOM, zorla CI iptali) temizliği garanti etmek için varsayılan olarak bir “Ryuk” reaper container’ı kullanır — kendi temizlik garantiniz olmadıkça bunu devre dışı bırakmayın, aksi takdirde zamanla CI runner’larında container sızdırırsınız.
- Image sabitleme: tekrarlanabilir CI çalıştırmaları için her zaman container image tag’lerini sabitleyin (
postgres:latestdeğilpostgres:16.4-alpine) — bir base image’daki sessiz bir minor sürüm artışı birçok pipeline’ı bozmuştur.
10.9 Testcontainers’a karşı Altyapıyı Mock’lamak — Karar Çerçevesi
| Senaryo | Öneri |
|---|---|
| Kendi iş mantığınızı test etmek, I/O yok | Unit test, container yok |
| Repository/DAO SQL doğruluğunu test etmek | Testcontainers (gerçek Postgres/MySQL) |
| Sadece mesaj serialization/deserialization mantığını test etmek | Fake bir mesaj yolu ile unit test |
| Servisinizin gerçekten Kafka’ya doğru şekilde, gerçek serialization ile yayın yaptığını test etmek | Testcontainers (gerçek Kafka) |
| Kararsız bir bağımlılığa karşı retry/backoff mantığını test etmek | N kez başarısız olup sonra başarılı olan bir Mock ile unit test |
| Tam istek → DB → yanıt akışını test etmek | Integration test, FastAPI’nin TestClient‘ı/httpx.AsyncClient + Testcontainers Postgres |
| Servisler arası sözleşmeleri test etmek | Contract testing (örn. Pact) veya özel bir E2E ortamı, Testcontainers değil |
11. Test Mimarisi ve Proje Yapısı
11.1 Dizin Yapısı
myproject/
├── src/
│ └── myapp/
│ ├── domain/
│ ├── services/
│ └── repositories/
├── tests/
│ ├── conftest.py # paylaşılan fixture'lar (factory'ler, faker seeding)
│ ├── unit/
│ │ ├── conftest.py # sadece unit fixture'ları — docker import'u YOK
│ │ ├── domain/
│ │ │ └── test_pricing.py
│ │ └── services/
│ │ └── test_order_service.py
│ ├── integration/
│ │ ├── conftest.py # Testcontainers fixture'ları SADECE burada yaşar
│ │ └── repositories/
│ │ └── test_postgres_user_repository.py
│ └── e2e/
│ └── test_checkout_flow.py
├── pyproject.toml
└── Makefile
Bunun neden önemli olduğu: Testcontainers/Docker import’larını sıkı bir şekilde tests/integration/conftest.py içinde tutmak (asla üst düzey tests/conftest.py‘de değil), pytest tests/unit çalıştırmanın asla Docker’ın kurulu olmasını gerektirmediği anlamına gelir — bu, hızlı yerel geliştirme döngüleri ve Docker erişimi olmayan katkıda bulunanlar için kritiktir.
11.2 Test Veri Builder’ları / Object Mother Deseni
Ayrıntılı nesne oluşturmayı onlarca test arasında tekrarlamaktan kaçının:
# tests/factories.py
import factory
from myapp.domain import User, Order
class UserFactory(factory.Factory):
class Meta:
model = User
id = factory.Sequence(lambda n: n)
email = factory.LazyAttribute(lambda o: f"user{o.id}@example.com")
tier = "standard"
class OrderFactory(factory.Factory):
class Meta:
model = Order
id = factory.Sequence(lambda n: n)
user = factory.SubFactory(UserFactory)
total = 100.0
def test_premium_user_gets_discount():
user = UserFactory(tier="premium")
order = OrderFactory(user=user, total=200.0)
assert apply_discount(order) == 180.0
factory_boy (veya elle yazılmış builder fonksiyonları), her testin sadece önemsediği şeyi override etmesine izin verir, geri kalanını varsayılanlar halleder — bu, User/Order şeması yeni zorunlu alanlar kazandığında test kırılganlığını önemli ölçüde azaltır.
11.3 Web Framework’leri için Test Client Deseni
# FastAPI
from fastapi.testclient import TestClient
@pytest.fixture
def client(app_with_test_db):
return TestClient(app_with_test_db)
def test_create_order_endpoint(client):
response = client.post("/orders", json={"item_id": 1, "quantity": 2})
assert response.status_code == 201
assert response.json()["status"] == "pending"
# Django
import pytest
@pytest.mark.django_db
def test_create_order_view(client):
response = client.post("/api/orders/", {"item_id": 1, "quantity": 2})
assert response.status_code == 201
pytest-django‘nun @pytest.mark.django_db‘si her testi bir transaction’a sarar ve geri alır — yukarıdaki Testcontainers örneğiyle aynı rollback-izolasyon deseni, Django’nun ORM’sine uygulanmıştır.
11.4 Test Edilebilirlik için Dependency Injection
Kendi bağımlılıklarını dahili olarak oluşturan kod test edilmesi zordur:
# TEST ETMESİ ZOR — gizli bağımlılık
class OrderService:
def __init__(self):
self.db = PostgresConnection() # testlerde değiştirilemez
self.email = SmtpEmailSender()
# TEST ETMESİ KOLAY — bağımlılıklar enjekte edilmiş
class OrderService:
def __init__(self, db: Database, email: EmailSender):
self.db = db
self.email = email
Bu “test edilebilirlik için aşırı mühendislik” değildir — bu, Bağımlılığın Tersine Çevrilmesi İlkesi’dir (Dependency Inversion Principle) ve bir kod tabanını herhangi bir seviyede test edilebilir kılmak için en yüksek kaldıraçlı tek tasarım kararıdır.
12. Coverage, Mutation Testing ve Test Kalitesi
12.1 Kod Coverage’ı — Gerçekte Size Ne Söylüyor
pytest --cov=myapp --cov-report=term-missing --cov-report=html
Coverage size hangi satırların çalıştığını söyler, doğru şekilde test edilip edilmediğini değil. Sıfır assertion’lı bir test, hiçbir şeyi doğrulamadan %100 coverage üretebilir.
# %100 satır coverage, SIFIR doğrulama değeri
def test_process_order():
process_order(Order(id=1, total=100))
# assertion yok!
Coverage’ı optimize edilecek bir hedef olarak değil, boşluklar için bir teşhis aracı olarak kullanın. CI’da bir coverage eşik geçidi (örn. “coverage < %80 ise build’i başarısız yap”) bir taban olarak makuldür, ancak %100 coverage’ı kovalamak genellikle asıl riskli mantığı kaçırırken önemsiz getter/setter’ların düşük değerli testlerini üretir.
12.2 Branch Coverage
[tool.coverage.run]
branch = true
Satır coverage’ı test edilmemiş branch’leri gizleyebilir:
def get_discount(user):
if user.is_premium:
return 0.2
return 0.0
get_discount(premium_user)‘ı çağıran tek bir test, %100 satır coverage’ı verir (her satır çalışır) ama sadece %50 branch coverage’ı verir (else yolu hiç alınmaz). Her zaman branch coverage’ı etkinleştirin.
12.3 Mutation Testing — Testlerinizi Test Etmek
Coverage size kodun çalıştığını söyler; bir hatayı yakalayıp yakalamayacağını söylemez. Mutation testing (mutmut veya cosmic-ray aracılığıyla), kaynağınıza otomatik olarak küçük hatalar (“mutant”) ekler — <‘yi <=‘ye, and‘i or‘a çevirir, bir satırı kaldırır — ve test paketinizi yeniden çalıştırır. Testler hala geçiyorsa, mutant hayatta kalmıştır, yani testleriniz aslında o mantığı doğrulamamıştır.
pip install mutmut
mutmut run --paths-to-mutate=src/myapp/domain/
mutmut results
# Orijinal
def is_eligible(age):
return age >= 18
# Mutant: age >= 18 -> age > 18
def is_eligible(age):
return age > 18
Test paketiniz test_is_eligible_at_exactly_18‘i içermiyorsa, bu mutant hayatta kalır — satır/branch coverage’ın asla ortaya çıkaramayacağı bir sınır koşulu boşluğunu ortaya çıkarır. Mutation testing pahalıdır (büyük bir kod tabanında saatler sürebilir), bu yüzden genellikle her commit’te değil, geceleri veya kritik modüller (fiyatlandırma, auth, ödeme mantığı) için talep üzerine çalıştırılır.
12.4 Flaky Test Yönetimi
Flaky bir test (deterministik olmayan şekilde geçer/başarısız olur), testin hiç olmamasından daha kötüdür — tüm paketteki güveni aşındırır (“CI’ı yeniden çalıştır, muhtemelen flaky’dir” kültürel bir kanserdir).
Yaygın nedenler ve çözümler:
| Neden | Çözüm |
|---|---|
Gerçek sleep()/zamanlama varsayımları | Zaman aşımı ile polling veya sahte saatler (freezegun, time-machine) kullanın |
| Testler arasında paylaşılan mutable global durum | function scope’lu fixture’lar, autouse sıfırlama fixture’ları |
| Test sırası bağımlılığı | Sıra bağımlılıklarını bilerek ortaya çıkarmak için pytest-randomly ile çalıştırın |
| Seed’lenmemiş rastgelelik | Testlerde rastgele seed’leri sabitleyin (random.seed(42)) veya Hypothesis’in deterministik replay’ini kullanın |
| Gerçek harici servislere network çağrıları | Asla — her zaman fake/mock/Testcontainers |
| Container hazır değil (yarış durumu) | Uygun bekleme stratejileri, asla ham sleep() |
pytest-rerunfailures, bilinen flaky testleri geçici bir çözüm olarak yeniden deneyebilir, ancak bu kalıcı bir özellik değil, teknik borç olarak takip edilmelidir:
pytest --reruns 2 --reruns-delay 1 -m flaky
13. CI/CD Entegrasyonu
13.1 Aşamalı Pipeline
# .github/workflows/test.yml (örnek amaçlı)
name: Test
on: [push, pull_request]
jobs:
static-analysis:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install ruff mypy
- run: ruff check .
- run: mypy src/
unit-tests:
runs-on: ubuntu-latest
needs: static-analysis
steps:
- uses: actions/checkout@v4
- run: pip install -e ".[test]"
- run: pytest tests/unit -m "not slow" --cov=myapp --cov-fail-under=80
integration-tests:
runs-on: ubuntu-latest
needs: unit-tests
services:
docker:
image: docker:dind
steps:
- uses: actions/checkout@v4
- run: pip install -e ".[test]"
- run: pytest tests/integration --maxfail=1
e2e-tests:
runs-on: ubuntu-latest
needs: integration-tests
if: github.ref == 'refs/heads/main'
steps:
- run: pytest tests/e2e
İlke: önce hızlı ve ucuz olanla başarısız ol. Statik analiz (saniyeler) → unit testler (saniyeler ile birkaç dakika) → Testcontainers ile integration testler (dakikalar) → E2E (en yavaş, en az sıklıkta çalıştırılır, örn. sadece main‘de veya geceleri).
13.2 Paralelleştirme
pip install pytest-xdist
pytest -n auto # CPU çekirdekleri arasında paralelleştir
Paralelleştirme, izole olmayan testlerin acımasızca ortaya çıktığı yerdir — paylaşılan geçici dosyalar, paylaşılan portlar, paylaşılan global durum, -n auto altında hepsi bozulur. Bu, daha önce göz ardı ettiğiniz izolasyon hatalarını düzeltmek için iyi bir zorlayıcı fonksiyondur.
Özellikle Testcontainers için, session scope’lu container’lar normalde xdist altında tek bir worker süreci içinde paylaşılır — her worker kendi container örneğini alır, bu genellikle istediğiniz şeydir (worker’lar arası izolasyon) ama N worker = N container anlamına gelir, bu yüzden CI kaynaklarını buna göre bütçeleyin.
13.3 CI’da Docker Katmanlarını/Image’ları Önbelleğe Alma
CI önbellek adımında image’ları önceden çekmek, her paralel işin postgres:16-alpine‘i sıfırdan yeniden indirmesini önler, bu ölçekte önemlidir:
- run: docker pull postgres:16-alpine
- run: docker pull redis:7-alpine
14. Anti-Pattern’ler ve Yaygın Tuzaklar
Davranış yerine implementasyon detaylarını test etmek. Asıl önemli olan gözlemlenebilir çıktı iken, dahili özel durumu (
obj._cache) veya bir Mock üzerindeki tam çağrı sıralarını doğrulamak. Bu, testleri zararsız refactor’lara karşı kırılgan yapar.“Dondurma Külahı” (Ice Cream Cone) anti-piramidi. Çoğunlukla E2E/manuel testler, az unit test. Yavaş geri bildirim, pahalı CI, flaky paketler — sağlıklı bir piramidin tersi.
Sleep tabanlı senkronizasyon. “Asenkron işi beklemek” için
time.sleep(1)hem yavaştır (her zaman maliyeti öder) hem de güvenilmezdir (bazen yeterince uzun değildir). Açık polling/bekleme yardımcıları veya sahte bir saatin dependency injection’ını kullanın.Tanrı fixture’lar. Tüm uygulama bağlamını oluşturan ve her tek teste bağımlı olunan, izole, hızlı unit testlerin amacını boşa çıkaran tek bir
conftest.pyfixture’ı.Test arası bağımlılık. Test B, Test A’nın önce çalıştığını ve arkasında durum bıraktığını varsayar. Bunu yakalamak için düzenli olarak
pytest-randomlyile çalıştırın; bu hiçbir zaman mümkün olmamalıdır.Sahip olmadığınız şeyi aşırı mock’lamak. Üçüncü taraf kütüphane dahiliyetlerini kapsamlı bir şekilde mock’lamak, testlerinizi o kütüphanenin implementasyon detaylarına bağlar ve kütüphanenin gerçek davranışı değiştiğinde (örn. bir SDK’nın retry mantığı) yanlış bir güven verir. Gerçek servisin Testcontainers tabanlı bir fake’ine karşı test etmeyi veya kütüphanenin kendisinin sağladığı iyi bakımlı bir fake’i tercih edin.
Okunmaz test isimleri üreten aşırı parametrizasyon.
test_thing[True-False-None-3-<object at 0x7f>]— her zaman açıkid=değerleri verin.Tüm nesnenin sabit kodlanmış bir dict/JSON blob’una eşit olduğunu doğrulamak. İlgisiz alan eklemelerine karşı kırılgandır. Testle ilgili belirli alanları veya şema tabanlı kısmi bir eşleşmeyi doğrulamayı tercih edin.
Global/singleton durumu sıfırlamamak. Python’da özellikle modül seviyesi önbellekleme,
lru_cacheve singleton desenleri nedeniyle tehlikelidir — testler arasında singleton’ların sıfırlandığını doğrulayın, özelliklepytest-xdistworker yeniden kullanımı göz önüne alındığında.Testleri düzeltmek yerine atlamak. JIRA bildirimi/son kullanma tarihi olmadan zamanla biriken
@pytest.mark.skip, herkesin unuttuğu test edilmemiş kod yollarının bir mezarlığına dönüşür.
15. Principal Seviye Test Paketleri için Kontrol Listesi
- Unit testler tüm paket için (binlerce test) sıfır I/O ile 10 saniyenin altında çalışır.
- Integration testler mock’lanmış ORM’ler/sürücüler yerine Testcontainers ile gerçek altyapı kullanır.
-
conftest.pyyapısı, unit testlerin Docker’a bağımlı kodu asla import etmemesini zorunlu kılar. - Fixture’lar varsayılan olarak
functionscope’ludur; daha geniş scope’lar gerekçelendirilmiştir (pahalı, salt-okunur/doğal olarak paylaşılan kaynaklar). - Önemsiz olmayan tüm mock’lar için
autospec=Truekullanılır. - Mimari birleşim noktalarında mock yerine fake tercih edilir; mock’lar etkileşimleri/yan etkileri doğrulamak için ayrılmıştır.
- Branch coverage etkindir, coverage bir gösteriş metriği değil, teşhis amaçlı kullanılır.
- Mutation testing kritik iş mantığı (fiyatlandırma, auth, ödemeler) üzerinde periyodik olarak çalıştırılır.
- CI pipeline aşamalıdır: statik analiz → unit → integration → E2E, hızlı-başarısızlık sıralamasıyla.
- Container image’ları belirli sürümlere sabitlenmiştir; Ryuk reaper etkindir.
- Paketin hiçbir yerinde senkronizasyon için
time.sleep()kullanılmaz. - Test isimleri kendi kendini belgeler; başarısız bir test isminin kendisi neyin bozulduğunu söyler.
- Property-based testler (Hypothesis) önemsiz olmayan girdi uzaylarına sahip saf fonksiyonlar için kullanılır.
- Flaky testler bir sahip ve son tarih ile teknik borç olarak takip edilir, sessizce sonsuza kadar yeniden çalıştırılmaz.
- Test verisi kopyala-yapıştır ayrıntılı nesne oluşturma yerine factory/builder’lar aracılığıyla oluşturulur.
Ek: Minimal Referans pyproject.toml
[project.optional-dependencies]
test = [
"pytest>=8.0",
"pytest-cov>=5.0",
"pytest-xdist>=3.5",
"pytest-randomly>=3.15",
"pytest-asyncio>=0.23",
"hypothesis>=6.100",
"testcontainers[postgres,kafka,redis,localstack]>=4.0",
"factory-boy>=3.3",
"freezegun>=1.5",
]
[tool.pytest.ini_options]
minversion = "8.0"
addopts = "-ra -q --strict-markers --strict-config --cov=myapp --cov-report=term-missing"
testpaths = ["tests"]
markers = [
"slow: uzun süren testler",
"integration: Docker/Testcontainers gerektirir",
"e2e: tam uçtan uca sistem testleri",
]
filterwarnings = ["error"]
[tool.coverage.run]
branch = true
source = ["src/myapp"]
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"if TYPE_CHECKING:",
"raise NotImplementedError",
]
Bu rehber, 2025–2026 civarında yüksek olgunluğa sahip Python mühendislik organizasyonlarının kullandığı pratikleri yansıtmaktadır. Araçlar ve API’ler zamanla gelişir — production kodunda tam sözdizimine güvenmeden önce her zaman güncel kütüphane dokümantasyonunu (pytest, Testcontainers, Hypothesis) kontrol edin.