Yazılım Test Türleri: Unit, Entegrasyon ve E2E Test Nedir?
Bir uygulamayı canlıya almadan önce “acaba bu çalışıyor mu?” diye sorduğunuzda aklınıza gelen ilk şey muhtemelen elle test yapmaktır. Uygulamayı açarsınız, birkaç butona tıklarsınız, bir form doldurursunuz ve “tamam çalışıyor” dersiniz. Ama bu yaklaşımın ne kadar kırılgan olduğunu, bir release gecesi saat 23:00’de production’da beklenmedik bir hata ile karşılaştığınızda anlarsınız. İşte bu yüzden yazılım test stratejileri, modern sysadmin ve DevOps dünyasında artık bir “geliştirici meselesi” olmaktan çıkmış, altyapı ve dağıtım süreçlerinin ayrılmaz bir parçası haline gelmiştir.
Bu yazıda unit test, entegrasyon testi ve end-to-end (E2E) test kavramlarını sadece teorik olarak değil, pipeline’larınızı kurarken, CI/CD süreçlerinizi tasarlarken ve geliştirici ekipleriyle konuşurken işinize yarayacak pratik bir perspektiften ele alacağız.
Test Piramidi: Neden Önemli?
Mike Cohn’un ortaya attığı “test piramidi” kavramını muhtemelen bir yerlerde duymuşsunuzdur. Piramidin tabanında unit testler, ortasında entegrasyon testleri, tepesinde ise E2E testler bulunur. Bu yapının anlamı şudur: tabana ne kadar yakınsanız testler o kadar hızlı, ucuz ve izole çalışır. Tepeye çıktıkça testler yavaşlar, bakımı zorlaşır ve kırılganlık artar.
Bir sysadmin olarak bu piramidi şöyle okuyabilirsiniz: eğer CI/CD pipeline’ınız her commit’te 45 dakika sürüyorsa, muhtemelen piramidin tepesini fazla şişirip tabanını ihmal etmişsinizdir. Bu durum hem geliştirici deneyimini mahveder hem de feedback loop’ları uzatır.
Unit Test Nedir?
Unit test, bir yazılımın en küçük bağımsız parçasını, yani bir fonksiyonu, bir metodu veya bir sınıfı izole ederek test etmektir. “İzole” kelimesi burada kritik: unit test çalışırken dış bağımlılıklar (veritabanı, network, dosya sistemi) devreye girmez. Bunlar mock veya stub adı verilen sahte bileşenlerle ikame edilir.
Peki bu size ne sağlar? Bir bug’ı tam olarak nerede olduğunu söyler. Unit test başarısız olduğunda “şu fonksiyonda şu koşulda sorun var” diye işaret eder. 3000 satırlık bir uygulamada needle in a haystack aramak zorunda kalmazsınız.
Python ile Basit Bir Unit Test Örneği
# calculator.py
def divide(a, b):
if b == 0:
raise ValueError("Sifira bolme hatasi")
return a / b
# test_calculator.py
import pytest
from calculator import divide
def test_normal_division():
assert divide(10, 2) == 5.0
def test_zero_division_raises():
with pytest.raises(ValueError):
divide(10, 0)
def test_negative_numbers():
assert divide(-10, 2) == -5.0
Bu testi çalıştırmak için:
pip install pytest
pytest test_calculator.py -v
Çıktı şöyle görünür:
test_calculator.py::test_normal_division PASSED
test_calculator.py::test_zero_division_raises PASSED
test_calculator.py::test_negative_numbers PASSED
3 passed in 0.12s
0.12 saniye. Veritabanı yok, network yok, hiçbir dış bağımlılık yok. Bu hızı CI/CD pipeline’larınızda çoğaltabilirsiniz: yüzlerce unit test 30 saniyenin altında tamamlanır.
Mock Kullanımı: Dış Bağımlılıkları Yönetmek
Gerçek dünyada fonksiyonlarınız çoğunlukla dış servislerle konuşur. Bir API çağrısı yapan fonksiyonu test etmek istediğinizde gerçek API’yi çağırmak istemezsiniz; hem yavaştır hem de test ortamınızın internet erişimine bağımlı hale gelir.
from unittest.mock import patch, MagicMock
import requests
def get_server_status(ip_address):
response = requests.get(f"http://{ip_address}/health")
if response.status_code == 200:
return "healthy"
return "unhealthy"
def test_healthy_server():
mock_response = MagicMock()
mock_response.status_code = 200
with patch('requests.get', return_value=mock_response):
result = get_server_status("192.168.1.100")
assert result == "healthy"
def test_unhealthy_server():
mock_response = MagicMock()
mock_response.status_code = 503
with patch('requests.get', return_value=mock_response):
result = get_server_status("192.168.1.100")
assert result == "unhealthy"
Bu yaklaşımla requests.get asla gerçek bir HTTP isteği atmaz. Test tamamen izole çalışır. Bu, özellikle ödeme sistemleri, SMS gateway’leri veya üçüncü parti monitoring API’leriyle çalışan fonksiyonları test ederken hayat kurtarır.
Entegrasyon Testi Nedir?
Unit testler tek bir parçanın doğru çalıştığını söyler. Ama iki parça bir araya geldiğinde de düzgün çalışıyor mu? İşte entegrasyon testlerinin cevapladığı soru budur.
Klasik bir örnek: veritabanı katmanınız unit testlerde mükemmel çalışıyor, iş mantığı katmanınız da. Ama ikisi bir araya geldiğinde SQL sorgusu yanlış parametrelerle çağrılıyor ve uygulama patlıyor. Bunu ancak entegrasyon testi yakalar.
Entegrasyon testleri gerçek bileşenleri kullanır: gerçek (ama genellikle test amaçlı) bir veritabanı, gerçek bir cache servisi, gerçek bir message queue. Bu yüzden unit testlere göre daha yavaş ve kurulumu daha karmaşıktır.
Docker ile Test Ortamı Kurma
Modern entegrasyon testlerinde Docker Compose kullanmak neredeyse standart haline geldi. Test koşmadan önce bağımlı servisleri ayağa kaldırırsınız, testleri çalıştırırsınız, bitince her şeyi silersiniz.
# docker-compose.test.yml
version: '3.8'
services:
db:
image: postgres:15
environment:
POSTGRES_DB: testdb
POSTGRES_USER: testuser
POSTGRES_PASSWORD: testpass
ports:
- "5432:5432"
redis:
image: redis:7-alpine
ports:
- "6379:6379"
app:
build: .
depends_on:
- db
- redis
environment:
DATABASE_URL: postgresql://testuser:testpass@db:5432/testdb
REDIS_URL: redis://redis:6379
command: pytest tests/integration/ -v
Bu compose dosyasını şöyle çalıştırırsınız:
docker-compose -f docker-compose.test.yml up --abort-on-container-exit
docker-compose -f docker-compose.test.yml down -v
--abort-on-container-exit flag’i app container’ı bittiğinde tüm stack’i kapatır. -v flag’i ise volume’ları da temizler, böylece her test koşumu temiz bir slate ile başlar.
Gerçek Bir Entegrasyon Testi Örneği
# tests/integration/test_user_repository.py
import pytest
import psycopg2
from app.repositories import UserRepository
@pytest.fixture(scope="module")
def db_connection():
conn = psycopg2.connect(
host="localhost",
database="testdb",
user="testuser",
password="testpass"
)
yield conn
conn.close()
@pytest.fixture(autouse=True)
def clean_users_table(db_connection):
yield
cursor = db_connection.cursor()
cursor.execute("DELETE FROM users")
db_connection.commit()
def test_create_and_retrieve_user(db_connection):
repo = UserRepository(db_connection)
user_id = repo.create(username="ali.veli", email="[email protected]")
retrieved = repo.find_by_id(user_id)
assert retrieved["username"] == "ali.veli"
assert retrieved["email"] == "[email protected]"
def test_duplicate_email_raises_error(db_connection):
repo = UserRepository(db_connection)
repo.create(username="user1", email="[email protected]")
with pytest.raises(Exception, match="duplicate key"):
repo.create(username="user2", email="[email protected]")
Burada dikkat çeken bir nokta: clean_users_table fixture’ı her testten sonra tabloyu temizliyor. Test izolasyonu entegrasyon testlerinde de kritik; bir testin bıraktığı veri bir sonrakini etkilememelidir.
E2E Test Nedir?
End-to-end test, uygulamayı gerçek bir kullanıcı gibi baştan sona test etmektir. Tarayıcıyı açar, bir URL’ye gider, butona tıklar, form doldurur, sonucu doğrular. Tüm sistem stack’i yerli yerindedir: frontend, backend, veritabanı, belki üçüncü parti servisler.
Bu testlerin değeri tartışılmaz: gerçek kullanıcı deneyimini simüle eder ve hiçbir başka test türünün yakalayamayacağı sorunları bulur. Mesela JavaScript’in bir browser versiyonunda farklı davranması, CSS’in bir elementin üstüne gelmesi ve tıklanabilirliği engellemesi, sayfa yükleme sırasındaki race condition’lar… Bunların hiçbiri unit veya entegrasyon testinde görünmez.
Ama bedeli vardır: yavaştır, flaky’dir (bazen nedensiz başarısız olur), bakımı en maliyetlidir. Bu yüzden E2E testleri kritik kullanıcı akışları için kullanılır: kayıt ol, giriş yap, ödeme yap, sipariş ver gibi.
Playwright ile E2E Test
Playwright, Selenium’un yerini hızla alan modern bir E2E test aracıdır. Microsoft tarafından geliştirilen bu araç Chromium, Firefox ve WebKit üzerinde çalışır.
pip install playwright pytest-playwright
playwright install
# tests/e2e/test_login_flow.py
import pytest
from playwright.sync_api import Page, expect
def test_successful_login(page: Page):
page.goto("http://localhost:3000")
page.get_by_label("Kullanici Adi").fill("admin")
page.get_by_label("Sifre").fill("gizli123")
page.get_by_role("button", name="Giris Yap").click()
expect(page).to_have_url("http://localhost:3000/dashboard")
expect(page.get_by_text("Hos geldiniz, Admin")).to_be_visible()
def test_wrong_password_shows_error(page: Page):
page.goto("http://localhost:3000")
page.get_by_label("Kullanici Adi").fill("admin")
page.get_by_label("Sifre").fill("yanlissifre")
page.get_by_role("button", name="Giris Yap").click()
expect(page.get_by_text("Gecersiz kullanici adi veya sifre")).to_be_visible()
expect(page).to_have_url("http://localhost:3000/login")
E2E testleri headless modda çalıştırmak CI ortamları için şart:
pytest tests/e2e/ --headed=false -v
CI/CD Pipeline’ında Test Stratejisi
Üç test türünü de tanıdık. Şimdi asıl meseleye gelelim: bunları pipeline’ınıza nasıl entegre edersiniz?
Genel prensip şudur: hızlı testler önce çalışır, yavaş testler sonra. Fail fast prensibi: bir şey bozuksa bunu mümkün olan en erken aşamada öğrenmek istersiniz.
# .github/workflows/test.yml
name: Test Pipeline
on: [push, pull_request]
jobs:
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Python Kur
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Bagimlilik Yukle
run: pip install -r requirements.txt
- name: Unit Testleri Calistir
run: pytest tests/unit/ -v --tb=short
integration-tests:
runs-on: ubuntu-latest
needs: unit-tests
services:
postgres:
image: postgres:15
env:
POSTGRES_DB: testdb
POSTGRES_USER: testuser
POSTGRES_PASSWORD: testpass
ports:
- 5432:5432
redis:
image: redis:7
ports:
- 6379:6379
steps:
- uses: actions/checkout@v3
- name: Python Kur
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Entegrasyon Testleri Calistir
run: pytest tests/integration/ -v
env:
DATABASE_URL: postgresql://testuser:testpass@localhost:5432/testdb
REDIS_URL: redis://localhost:6379
e2e-tests:
runs-on: ubuntu-latest
needs: integration-tests
steps:
- uses: actions/checkout@v3
- name: Playwright Kur
run: |
pip install playwright pytest-playwright
playwright install chromium
- name: Uygulamayi Ayaga Kaldir
run: docker-compose up -d
- name: E2E Testleri Calistir
run: pytest tests/e2e/ -v
- name: Uygulamayi Durdur
if: always()
run: docker-compose down
needs direktifi sayesinde unit testler geçmeden entegrasyon testleri, entegrasyon testleri geçmeden E2E testleri çalışmaz. Bu cascade yaklaşımı hem zaman hem de compute kaynağı tasarrufu sağlar.
Test Coverage: Ne Kadar Yeterli?
Coverage, yani kod kapsama oranı, testlerinizin kodunuzun yüzde kaçını çalıştırdığını gösterir. Sık sorulan soru: “%80 coverage yeterli mi?” Dürüst cevap: rakam önemlidir ama tek başına anlamsızdır.
pytest tests/unit/ --cov=app --cov-report=html --cov-report=term-missing
Bu komut hem terminal çıktısı hem de HTML rapor üretir. Şöyle görünür:
Name Stmts Miss Cover Missing
-----------------------------------------------------
app/calculator.py 8 1 88% 15
app/user_service.py 45 6 87% 23-28, 67
app/payment.py 62 18 71% 45-52, 78-90
-----------------------------------------------------
TOTAL 115 25 78%
Missing sütunu hangi satırların test edilmediğini gösterir. Ödeme modülünde 71%? O kısmı detaylı incelemeniz gerekir. Kritik iş mantığı içeren modüller düşük coverage ile bırakılmamalıdır.
Coverage’ı CI’da zorunlu kılabilirsiniz:
pytest --cov=app --cov-fail-under=80
Bu komut coverage %80’nin altında kalırsa non-zero exit code döndürür ve pipeline fail olur.
Flaky Test Problemi ve Çözümleri
Özellikle E2E testlerde karşılaşılan “flaky test” problemi, bazen geçen bazen geçmeyen testlerdir. Sysadmin olarak bu durumla pipeline loglarınızda sürekli karşılaşırsınız ve geliştirici ekibinden “test kendi kendine geçti, tekrar deneyin” duyarsınız.
Flaky testlerin başlıca sebepleri:
- Race condition: Sayfa yüklenmeden önce elemente tıklamaya çalışmak
- Hardcoded bekleme süreleri:
time.sleep(3)gibi sabit beklemeler - Test izolasyonu eksikliği: Bir testin bıraktığı state diğerini etkiliyor
- External servis bağımlılığı: Üçüncü parti bir API’nin yavaş yanıt vermesi
Playwright’ın auto-waiting özelliği birinci ve ikinci problemi büyük ölçüde çözer. Manuel sleep yerine element-aware bekleme kullanın:
# Kotu yaklasim
import time
page.click("#submit-button")
time.sleep(3)
assert page.inner_text(".success-message") == "Basarili"
# Iyi yaklasim
page.click("#submit-button")
expect(page.locator(".success-message")).to_be_visible(timeout=10000)
Playwright, element görünür olana kadar 10 saniye bekler, daha erken görünürse hemen devam eder. Hem daha hızlı hem daha güvenilir.
Sonuç
Bu üç test türünü pipeline’ınıza entegre etmek başlangıçta fazladan iş gibi görünebilir. Ama bir production incident’ten sonra saatlerce log karıştırmak yerine, test raporunda “şu entegrasyon testi başarısız, sorun burada” yazan bir çıktıya bakmanın ne kadar değerli olduğunu bir kez yaşadığınızda bu yatırımın karşılığını anında görürsünüz.
Başlangıç için önerilen yaklaşım şu: önce kritik iş mantığınıza unit test yazın, 30 saniyenin altında çalışan bir test suit oluşturun. Sonra veritabanı ve servis entegrasyonlarını kapsayan entegrasyon testleri ekleyin. En son, beş-on kritik kullanıcı akışı için E2E testler yazın. Pipeline’ınızı bu sırayla çalışacak şekilde kurun.
Test yazmak geliştirici işi, pipeline kurmak sysadmin işi değildir artık. Modern altyapı yönetimi, kodun güvenilirliğini garanti altına alan bu mekanizmaları anlamayı ve işletmeyi gerektiriyor. CI/CD pipeline’ınızın kalitesi, büyük ölçüde test stratejinizin olgunluğuyla doğru orantılıdır.
