Selenium ile Web Uygulama Otomasyonu ve Test Yazımı

Bir prodüksiyon ortamında gecenin köründe patlayan bir web uygulamasını düşünün. QA ekibi “ben test ettim” diyor, geliştirici “bende çalışıyor” diyor, ama kullanıcılar form gönderemediklerini raporluyor. İşte tam bu noktada “acaba selenium ile otomatik test yazsaydık…” diye geçiyor aklınızdan. Bu yazıda o pişmanlığı yaşamamanız için Selenium’u gerçek dünyada nasıl kullanacağınızı aktaracağım.

Selenium Nedir, Neden Hala Geçerli?

Selenium 2004’ten beri hayatımızda. “Playwright var, Cypress var, neden Selenium?” diye sorabilirsiniz. Haklısınız, alternatifler güçlendi. Ama şunu söyleyeyim: kurumsal ortamlarda, özellikle eski tarayıcı desteği gereken projelerde, Java ekosistemiyle entegrasyon gereken durumlarda Selenium hala birinci tercih. Üstelik Python binding’leri ile birlikte kullanmak son derece pratik.

Temel mimarisi şöyle çalışır: Selenium WebDriver, tarayıcıyı native API’si üzerinden kontrol eder. Firefox için GeckoDriver, Chrome için ChromeDriver ara katman olarak devreye girer. Siz Python ya da Java ile komut yazarsınız, bu komutlar WebDriver protokolüyle tarayıcıya iletilir.

Kurulum ve Ortam Hazırlığı

Önce temiz bir sanal ortam oluşturalım. Sistem Python’unu kirletmek istemeyiz:

python3 -m venv selenium-env
source selenium-env/bin/activate
pip install selenium webdriver-manager pytest pytest-html

webdriver-manager paketi ChromeDriver versiyonunu Chrome’unuzla otomatik eşleştiriyor, bu sayede “driver uyumsuzluğu” derdi ortadan kalkıyor. Eskiden her Chrome güncellemesinden sonra driver indirip ayarlamak vardı, o günleri hatırlayanlar bilir.

Headless ortam için, yani CI/CD pipeline’larında veya sunucuda çalıştırırken xvfb gerekebilir:

sudo apt-get install -y google-chrome-stable xvfb
export DISPLAY=:99
Xvfb :99 -screen 0 1920x1080x24 &

Jenkins’te çalışırken bu satırları job başına eklemeyi unutmayın. Özellikle Docker container içinde headless Chrome kullanıyorsanız şu bayrakları mutlaka ekleyin:

# Dockerfile içinde veya test başlangıcında
google-chrome --headless --no-sandbox --disable-dev-shm-usage --remote-debugging-port=9222

--no-sandbox ve --disable-dev-shm-usage bayrakları olmadan Docker ortamında Chrome sürekli crash atar. Bunu ilk öğrendiğimde saatlerce uğraşmıştım.

İlk WebDriver Bağlantısı ve Temel Yapı

Basit bir senaryo ile başlayalım. Bir şirketin iç intranet login sayfasını test ediyoruz:

from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from webdriver_manager.chrome import ChromeDriverManager

def create_driver(headless=True):
    options = Options()
    if headless:
        options.add_argument("--headless")
    options.add_argument("--no-sandbox")
    options.add_argument("--disable-dev-shm-usage")
    options.add_argument("--window-size=1920,1080")
    
    service = Service(ChromeDriverManager().install())
    driver = webdriver.Chrome(service=service, options=options)
    driver.implicitly_wait(10)
    return driver

def test_login():
    driver = create_driver()
    try:
        driver.get("https://intranet.sirketim.com/login")
        
        username_field = driver.find_element(By.ID, "username")
        password_field = driver.find_element(By.ID, "password")
        
        username_field.clear()
        username_field.send_keys("test_user")
        password_field.send_keys("test_password123")
        
        login_btn = driver.find_element(By.CSS_SELECTOR, "button[type='submit']")
        login_btn.click()
        
        WebDriverWait(driver, 15).until(
            EC.presence_of_element_located((By.CLASS_NAME, "dashboard-container"))
        )
        
        assert "Dashboard" in driver.title
        print("Login testi başarılı!")
    finally:
        driver.quit()

Burada dikkat edilmesi gereken nokta implicitly_wait ile WebDriverWait farkı. implicitly_wait global bir bekleme süresi koyar; her element aramasında bu kadar bekler. WebDriverWait ise spesifik bir condition için akıllıca bekler. İkisini birlikte kullanmak zaman zaman beklenmedik davranışlara yol açabiliyor, tercihen WebDriverWait kullanmak daha öngörülebilir sonuçlar veriyor.

Page Object Model: Gerçek Hayatta Böyle Kullanılır

Düz test fonksiyonları yazmak başlangıç için iyi, ama ekip büyüdükçe kaos başlar. Page Object Model (POM) bu kaosa çözüm getiriyor. Her sayfayı bir sınıf olarak modelliyorsunuz:

# pages/login_page.py
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

class LoginPage:
    URL = "https://intranet.sirketim.com/login"
    
    USERNAME_INPUT = (By.ID, "username")
    PASSWORD_INPUT = (By.ID, "password")
    SUBMIT_BUTTON = (By.CSS_SELECTOR, "button[type='submit']")
    ERROR_MESSAGE = (By.CLASS_NAME, "error-alert")
    
    def __init__(self, driver):
        self.driver = driver
        self.wait = WebDriverWait(driver, 15)
    
    def open(self):
        self.driver.get(self.URL)
        return self
    
    def enter_username(self, username):
        field = self.wait.until(EC.element_to_be_clickable(self.USERNAME_INPUT))
        field.clear()
        field.send_keys(username)
        return self
    
    def enter_password(self, password):
        field = self.wait.until(EC.element_to_be_clickable(self.PASSWORD_INPUT))
        field.send_keys(password)
        return self
    
    def click_login(self):
        self.wait.until(EC.element_to_be_clickable(self.SUBMIT_BUTTON)).click()
        return self
    
    def get_error_message(self):
        try:
            return self.wait.until(
                EC.visibility_of_element_located(self.ERROR_MESSAGE)
            ).text
        except:
            return None
    
    def login(self, username, password):
        return (self.open()
                    .enter_username(username)
                    .enter_password(password)
                    .click_login())

Method chaining kullandım burada. Testler çok daha okunabilir oluyor:

# tests/test_login.py
import pytest
from pages.login_page import LoginPage
from pages.dashboard_page import DashboardPage

class TestLogin:
    
    def test_gecerli_kimlik_bilgileriyle_giris(self, driver):
        login_page = LoginPage(driver)
        login_page.login("gecerli_kullanici", "dogru_sifre")
        
        dashboard = DashboardPage(driver)
        assert dashboard.is_loaded(), "Dashboard yüklenmedi"
        assert dashboard.get_welcome_message() == "Hoş geldiniz, gecerli_kullanici"
    
    def test_yanlis_sifre_hata_mesaji(self, driver):
        login_page = LoginPage(driver)
        login_page.login("gecerli_kullanici", "yanlis_sifre")
        
        error = login_page.get_error_message()
        assert error is not None, "Hata mesajı görünmüyor"
        assert "Kullanıcı adı veya şifre hatalı" in error
    
    def test_bos_form_gonderimi(self, driver):
        login_page = LoginPage(driver)
        login_page.open().click_login()
        
        error = login_page.get_error_message()
        assert error is not None

Pytest Fixture’ları ile Driver Yönetimi

Her test için driver açıp kapatmak zahmetli. Pytest fixture’ları bu işi otomatize ediyor:

# conftest.py
import pytest
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from webdriver_manager.chrome import ChromeDriverManager

@pytest.fixture(scope="session")
def driver_session():
    """Session boyunca tek driver - entegrasyon testleri için"""
    options = Options()
    options.add_argument("--headless")
    options.add_argument("--no-sandbox")
    options.add_argument("--disable-dev-shm-usage")
    
    service = Service(ChromeDriverManager().install())
    driver = webdriver.Chrome(service=service, options=options)
    driver.maximize_window()
    
    yield driver
    driver.quit()

@pytest.fixture(scope="function")
def driver(driver_session):
    """Her test için temiz state - cookie ve storage temizlenir"""
    driver_session.delete_all_cookies()
    driver_session.execute_script("window.localStorage.clear()")
    driver_session.execute_script("window.sessionStorage.clear()")
    yield driver_session

@pytest.fixture
def authenticated_driver(driver):
    """Login olmuş driver döndürür"""
    from pages.login_page import LoginPage
    LoginPage(driver).login("test_user", "test_pass123")
    yield driver

scope="session" ile browser bir kere açılıyor, tüm testler aynı browser üzerinde çalışıyor. Her test öncesi cookie ve storage temizleniyor. Bu yaklaşım test süresini ciddi ölçüde kısaltıyor.

Gerçek Dünya Senaryosu: E-Ticaret Sepet Testi

Teorik değil, gerçekten işe yarar bir senaryo yazalım. Bir e-ticaret uygulamasında “ürün sepete ekle ve sipariş tamamla” akışını test ediyoruz:

# tests/test_checkout_flow.py
import pytest
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.action_chains import ActionChains

class TestCheckoutFlow:
    
    def test_urun_sepete_ekle_ve_siparis_tamamla(self, authenticated_driver):
        driver = authenticated_driver
        wait = WebDriverWait(driver, 20)
        
        # Ürün sayfasına git
        driver.get("https://shop.sirketim.com/urunler")
        
        # İlk ürünün üzerine hover et, "Sepete Ekle" görünsün
        ilk_urun = wait.until(
            EC.presence_of_element_located((By.CSS_SELECTOR, ".product-card:first-child"))
        )
        ActionChains(driver).move_to_element(ilk_urun).perform()
        
        sepete_ekle_btn = wait.until(
            EC.element_to_be_clickable((By.CSS_SELECTOR, ".product-card:first-child .add-to-cart"))
        )
        urun_adi = ilk_urun.find_element(By.CLASS_NAME, "product-name").text
        sepete_ekle_btn.click()
        
        # Başarı bildirimi bekle
        wait.until(
            EC.visibility_of_element_located((By.CLASS_NAME, "toast-success"))
        )
        
        # Sepete git
        driver.get("https://shop.sirketim.com/sepet")
        
        # Sepette ürün var mı kontrol et
        sepet_urunleri = wait.until(
            EC.presence_of_all_elements_located((By.CLASS_NAME, "cart-item"))
        )
        
        urun_adlari = [item.find_element(By.CLASS_NAME, "item-name").text 
                       for item in sepet_urunleri]
        assert urun_adi in urun_adlari, f"{urun_adi} sepette bulunamadı"
        
        # Ödemeye geç
        odeme_btn = wait.until(
            EC.element_to_be_clickable((By.ID, "checkout-button"))
        )
        odeme_btn.click()
        
        # Ödeme sayfasının yüklendiğini doğrula
        wait.until(EC.url_contains("/odeme"))
        assert "Ödeme" in driver.title
        
        print(f"Checkout flow testi başarılı. Ürün: {urun_adi}")

Screenshot Alma ve Hata Raporlama

Test başarısız olduğunda ne oldu görmek istiyorsunuz. Screenshot almak hayat kurtarır:

# utils/screenshot_helper.py
import os
from datetime import datetime

def screenshot_al(driver, test_adi, basarili=False):
    zaman_damgasi = datetime.now().strftime("%Y%m%d_%H%M%S")
    klasor = "test_results/screenshots"
    os.makedirs(klasor, exist_ok=True)
    
    durum = "pass" if basarili else "fail"
    dosya_adi = f"{klasor}/{durum}_{test_adi}_{zaman_damgasi}.png"
    
    driver.save_screenshot(dosya_adi)
    return dosya_adi

# conftest.py içine eklenecek hook
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
    outcome = yield
    rep = outcome.get_result()
    
    if rep.when == "call" and rep.failed:
        driver = item.funcargs.get("driver")
        if driver:
            screenshot_yolu = screenshot_al(driver, item.name, basarili=False)
            print(f"nHata screenshot: {screenshot_yolu}")

Bu hook pytest’e entegre oluyor ve test başarısız olduğunda otomatik screenshot alıyor. CI/CD pipeline’ında artifact olarak saklayabilirsiniz.

Paralel Test Çalıştırma

50 test varsa sırayla çalışması dakikalar alır. pytest-xdist ile paralel çalıştırabilirsiniz:

# Kurulum
pip install pytest-xdist

# 4 worker ile çalıştır
pytest tests/ -n 4 --html=test_results/report.html --self-contained-html

# Sadece belirli marker'lı testleri çalıştır
pytest tests/ -m "smoke" -n 2 -v

# Başarısız testleri tekrar dene
pip install pytest-rerunfailures
pytest tests/ --reruns 2 --reruns-delay 3

Paralel çalıştırmada dikkat edilmesi gereken nokta: eğer testler aynı test verisi üzerinde işlem yapıyorsa çakışma olabilir. Her worker için izole test verisi kullanmak şart.

Jenkins Pipeline Entegrasyonu

Gerçek değer burada ortaya çıkıyor. Her commit’te testlerin otomatik çalışması:

# Jenkinsfile (Declarative Pipeline)
pipeline {
    agent {
        docker {
            image 'python:3.11-slim'
            args '--shm-size=2g'
        }
    }
    
    stages {
        stage('Hazırlık') {
            steps {
                sh '''
                    apt-get update -q
                    apt-get install -y wget gnupg unzip
                    wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | apt-key add -
                    echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list
                    apt-get update -q
                    apt-get install -y google-chrome-stable
                    pip install -r requirements.txt
                '''
            }
        }
        
        stage('Selenium Testleri') {
            steps {
                sh '''
                    export DISPLAY=:99
                    Xvfb :99 -screen 0 1920x1080x24 &
                    pytest tests/ 
                        -n 3 
                        --html=test_results/report.html 
                        --self-contained-html 
                        --reruns 1 
                        -v
                '''
            }
            post {
                always {
                    publishHTML([
                        allowMissing: false,
                        alwaysLinkToLastBuild: true,
                        keepAll: true,
                        reportDir: 'test_results',
                        reportFiles: 'report.html',
                        reportName: 'Selenium Test Raporu'
                    ])
                    archiveArtifacts artifacts: 'test_results/screenshots/**/*.png',
                                     allowEmptyArchive: true
                }
            }
        }
    }
}

--shm-size=2g Docker argümanı kritik. Chrome shared memory için yeterli alan bulamazsa crash atar. Bu parametreyi atlamak en sık yapılan hatalardan biri.

Selenium Grid ile Dağıtık Test

Farklı tarayıcılarda aynı anda test etmek istiyorsanız Selenium Grid devreye giriyor:

# Docker Compose ile Grid kurulumu
cat > docker-compose-grid.yml << 'EOF'
version: '3'
services:
  selenium-hub:
    image: selenium/hub:4.15
    ports:
      - "4442:4442"
      - "4443:4443"
      - "4444:4444"
  
  chrome-node:
    image: selenium/node-chrome:4.15
    shm_size: 2gb
    environment:
      - SE_EVENT_BUS_HOST=selenium-hub
      - SE_EVENT_BUS_PUBLISH_PORT=4442
      - SE_EVENT_BUS_SUBSCRIBE_PORT=4443
      - SE_NODE_MAX_SESSIONS=3
    depends_on:
      - selenium-hub
  
  firefox-node:
    image: selenium/node-firefox:4.15
    shm_size: 2gb
    environment:
      - SE_EVENT_BUS_HOST=selenium-hub
      - SE_EVENT_BUS_PUBLISH_PORT=4442
      - SE_EVENT_BUS_SUBSCRIBE_PORT=4443
    depends_on:
      - selenium-hub
EOF

docker-compose -f docker-compose-grid.yml up -d

Grid’e bağlanmak için driver oluştururken Remote WebDriver kullanıyorsunuz:

from selenium import webdriver
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities

driver = webdriver.Remote(
    command_executor="http://selenium-hub:4444/wd/hub",
    desired_capabilities=DesiredCapabilities.CHROME
)

Flaky Test Sorunları ve Çözümleri

Selenium testlerinin en büyük baş ağrısı “flaky test” problemi. Bazen geçiyor, bazen geçmiyor. Temel sebepler:

  • Timing sorunları: Sabit time.sleep() yerine explicit wait kullanın. time.sleep(3) yazan testi görünce içim burkuluyor.
  • Element stale referansı: DOM yeniden render olduğunda tuttuğunuz element referansı geçersiz kalıyor. StaleElementReferenceException alıyorsanız elementi tekrar bulun.
  • Animasyonlar: CSS animasyonu bitmeden tıklarsanız hata alırsınız. visibility_of_element_located yerine element_to_be_clickable kullanın.
  • Ağ gecikmesi: Test ortamındaki yavaş API yanıtları için wait sürelerini gerçekçi tutun.

Retry mekanizması eklemek de mantıklı:

from selenium.common.exceptions import StaleElementReferenceException
import time

def guvenli_tikla(driver, locator, max_deneme=3):
    for deneme in range(max_deneme):
        try:
            element = WebDriverWait(driver, 10).until(
                EC.element_to_be_clickable(locator)
            )
            element.click()
            return True
        except StaleElementReferenceException:
            if deneme < max_deneme - 1:
                time.sleep(1)
                continue
            raise
    return False

Sonuç

Selenium’u kurumsal ortamda doğru yapılandırıp CI/CD’ye entegre ettiğinizde gece yarısı telefonları ciddi ölçüde azalıyor. Deneyimlerimden çıkardığım özet:

  • Page Object Model olmadan büyüyen test suite’i yönetmek imkansız hale geliyor
  • WebDriverWait kullanın, time.sleep() değil
  • Docker ortamında --no-sandbox, --disable-dev-shm-usage ve --shm-size ayarlarını asla atlamamayın
  • Her başarısız testte screenshot alın, sabahleyin incelemek için altın değerinde
  • Paralel çalıştırma için test izolasyonuna dikkat edin
  • Flaky testleri tolere etmeyin, ekibin teste olan güvenini yok ediyor

Test yazmak zaman alıyor, doğru. Ama bir kez iyi bir altyapı kurduğunuzda, her yeni özellik için test eklemek alışkanlık haline geliyor. Ve o “bende çalışıyor” tartışmaları da ortadan kalkıyor.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir