← Back← Geri Python Learning Platform Python Öğrenme Platformu

Python Test FrameworksPython Test Framework'leri

A complete guide to pytest, Selenium WebDriver, and Playwright — with real-world examples, Java comparisons, and QA best practices.

pytest, Selenium WebDriver ve Playwright için kapsamlı rehber — gerçek dünya örnekleri, Java karşılaştırmaları ve QA en iyi pratikleri.

1.1 — What is pytest?pytest Nedir?

pytest is Python's most popular test framework — it replaces the verbose unittest with clean, readable tests that require no boilerplate classes. It auto-discovers tests in any file matching test_*.py or *_test.py, runs them in parallel with pytest-xdist, and generates beautiful HTML reports.

pytest, Python'ın en popüler test framework'üdür — boilerplate sınıf gerektirmeyen temiz ve okunabilir testlerle verbose unittest'in yerini alır. test_*.py veya *_test.py ile eşleşen dosyalardaki testleri otomatik keşfeder, pytest-xdist ile paralel çalıştırır ve güzel HTML raporları üretir.

Unlike Java's JUnit where every test must live inside a class, pytest lets you write a function and it just works — no extends TestCase, no @Test annotation.

Java'nın JUnit'inden farklı olarak her testin bir sınıf içinde olması gerekmez; pytest ile sadece bir fonksiyon yazarsınız ve çalışır — extends TestCase yok, @Test annotation yok.

Zero BoilerplateSıfır Şablon

No class required. A plain def test_* function is a test.Sınıf gerekmiyor. Düz bir def test_* fonksiyonu test olarak çalışır.

🔌

Rich Plugin EcosystemZengin Plugin Ekosistemi

pytest-html, pytest-xdist, allure-pytest, pytest-cov, pytest-mock.

🪄

Powerful FixturesGüçlü Fixture'lar

Dependency injection for setup/teardown. Better than JUnit @Before/@After.Setup/teardown için dependency injection. JUnit @Before/@After'dan daha güçlü.

📊

Parametrize

Data-driven tests with @pytest.mark.parametrize. No extra framework needed.Veri odaklı testler için @pytest.mark.parametrize. Ekstra framework gerekmez.

InstallationKurulum

bash
pip install pytest pytest-html pytest-xdist allure-pytest pytest-cov pytest-mock
Java Developer NoteJava Geliştirici Notu pytest = JUnit 5 + TestNG + Mockito combined. @pytest.fixture@BeforeEach, @pytest.mark.parametrize@CsvSource, and there's no need for extends TestCase. pytest = JUnit 5 + TestNG + Mockito bir arada. @pytest.fixture@BeforeEach, @pytest.mark.parametrize@CsvSource, extends TestCase yazmaya gerek yok.

1.2 — Project StructureProje Yapısı

A well-structured pytest project separates pages, tests, fixtures, and test data. Click folders to expand/collapse.

İyi yapılandırılmış bir pytest projesi sayfaları, testleri, fixture'ları ve test verilerini ayırır. Klasörleri açıp kapatmak için tıklayın.

📁 my-test-project/
📁 pages/
🐍base_page.pybase
🐍login_page.py
🐍dashboard_page.py
📁 tests/
🐍conftest.pyfixtures
🐍test_login.pytest
🐍test_checkout.pytest
🐍test_api.pytest
📁 test_data/
📄users.csv
📄test_cases.json
📁 utils/
🐍api_client.py
🐍db_helper.py
⚙️pytest.iniconfig
⚙️requirements.txt
⚙️.env

pytest.ini — ConfigurationYapılandırma

ini
[pytest]
testpaths = tests
python_files = test_*.py *_test.py
python_classes = Test*
python_functions = test_*
addopts =
    --html=reports/report.html
    --self-contained-html
    -v
    --tb=short
markers =
    smoke: Quick sanity tests
    regression: Full regression suite
    api: API tests only

1.3 — First Testİlk Test

☕ Java vs Python — Test StructureTest Yapısı
☕ Java (JUnit 5)
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class LoginTest {
    @Test
    void testValidLogin() {
        // Arrange
        String user = "admin";
        // Act
        boolean result = login(user, "pass");
        // Assert
        assertTrue(result, "Login should succeed");
    }
}
🐍 Python (pytest)
def test_valid_login():
    # No class needed!
    # Arrange
    user = "admin"
    # Act
    result = login(user, "pass")
    # Assert
    assert result, "Login should succeed"

# That's the whole file. No imports for basic tests.
💡 No class, no imports, no annotations. pytest auto-discovers any function starting with test_. The assert keyword works natively — pytest rewrites it to give detailed failure messages.💡 Sınıf yok, import yok, annotation yok. pytest, test_ ile başlayan her fonksiyonu otomatik keşfeder. assert anahtar kelimesi yerel çalışır — pytest onu ayrıntılı hata mesajları için yeniden yazar.

Test LifecycleTest Yaşam Döngüsü

Setup
fixture
Browser launch, DB connect, auth tokenTarayıcı açma, DB bağlantısı, auth token
Test BodyTest Gövdesi
def test_*
Navigate, act, assertGezin, işlem yap, doğrula
Teardown
after yield
Close browser, rollback DBTarayıcıyı kapat, DB'yi geri al
Report
html/allure
pytest-html, Allure

1.4 — Assertions

pytest rewrites plain assert statements to show detailed diffs on failure — no need for assertEquals, assertThat, or Hamcrest matchers.

pytest, hata durumunda ayrıntılı farkları göstermek için düz assert ifadelerini yeniden yazar — assertEquals, assertThat veya Hamcrest matcher'larına gerek yok.

python
# pytest assertions — plain Python, rich output on failure

def test_assertions():
    # Equality (assertEquals in Java)
    assert result == "PASS"
    assert count == 5

    # Truthiness (assertTrue / assertFalse)
    assert user.is_active
    assert not user.is_banned

    # Membership (contains / containsKey)
    assert "admin" in roles
    assert "error" not in response.text

    # Type check (instanceof)
    assert isinstance(result, list)

    # Exception expected (assertThrows in JUnit 5)
    import pytest
    with pytest.raises(ValueError, match="invalid"):
        parse_status("UNKNOWN")

    # Approximate equality (float comparison)
    assert response_time == pytest.approx(1.2, abs=0.1)

    # Custom failure message
    assert status == 200, f"Expected 200 but got {status}: {response.text}"

Failure Output ComparisonHata Çıktısı Karşılaştırması

pytest output
$ pytest tests/test_login.py -v

FAILED tests/test_login.py::test_valid_login
───────────────── FAILURES ─────────────────
FAILED test_valid_login

def test_valid_login():
result = {"status": "FAIL", "user": "admin"}
> assert result["status"] == "PASS"
E AssertionError: assert 'FAIL' == 'PASS'
E - FAIL
E + PASS

1 failed, 3 passed in 0.12s

1.5 — Fixtures

Fixtures are pytest's dependency injection system. A fixture is a function decorated with @pytest.fixture that provides setup (and optional teardown via yield) to any test that declares it as a parameter.

Fixture'lar, pytest'in bağımlılık enjeksiyon sistemidir. Fixture, parametre olarak bildiren herhangi bir teste setup (ve yield aracılığıyla isteğe bağlı teardown) sağlayan @pytest.fixture ile dekore edilmiş bir fonksiyondur.

python
import pytest
from playwright.sync_api import sync_playwright

@pytest.fixture(scope="session")
def browser():
    """Launch browser ONCE for all tests — expensive setup."""
    with sync_playwright() as p:
        b = p.chromium.launch(headless=True)
        yield b          # ← tests run here
        b.close()        # ← teardown after ALL tests

@pytest.fixture(scope="function")
def page(browser):
    """Fresh page for EACH test — injects 'browser' automatically."""
    context = browser.new_context(base_url="https://example.com")
    pg = context.new_page()
    yield pg             # ← test gets this
    context.close()      # ← runs after each test

@pytest.fixture
def logged_in_page(page):
    """Reuse 'page' fixture, add login step."""
    page.goto("/login")
    page.fill("#email", "admin@test.com")
    page.fill("#password", "secret")
    page.click("#submit")
    return page          # no teardown needed here

# ── Usage in test ──
def test_dashboard_title(logged_in_page):
    assert logged_in_page.title() == "Dashboard"

def test_user_count(logged_in_page):
    count = logged_in_page.locator(".user-row").count()
    assert count > 0

Fixture Scope — Lifecycle VisualizationFixture Scope — Yaşam Döngüsü Görselleştirmesi

function
Runs for EACH test — total isolation (default)Her test için çalışır — tam izolasyon (varsayılan)
class
Shared within a test class — setup once per classTest sınıfı içinde paylaşılır — sınıf başına bir kurulum
module
Shared per .py file — one setup per filePython dosyası başına paylaşılır — dosya başına bir kurulum
session
Once per entire test run — use for browser/DBTüm test çalışması boyunca bir kez — browser/DB için kullan
⚠️
Scope Mismatch ErrorScope Uyuşmazlığı Hatası A function-scoped fixture CANNOT depend on a narrower scope fixture. A session fixture can depend on session or nothing — not on function. Bir function scope'lu fixture, daha dar kapsamlı bir fixture'a bağımlı OLAMAZ. Bir session fixture yalnızca session'a bağımlı olabilir — function'a değil.

1.6 — Markers & Parametrize

python
import pytest

# ── Custom markers (defined in pytest.ini) ──
@pytest.mark.smoke
def test_homepage_loads(page):
    page.goto("/")
    assert page.title() == "Home"

@pytest.mark.regression
@pytest.mark.slow
def test_full_checkout_flow(logged_in_page):
    ...

# ── Skip / xfail ──
@pytest.mark.skip(reason="Feature not implemented yet")
def test_dark_mode():
    ...

@pytest.mark.xfail(reason="Known bug #123", strict=True)
def test_payment_with_expired_card(page):
    ...

# ── Parametrize — Data-Driven Tests ──
@pytest.mark.parametrize("email,password,expected", [
    ("admin@test.com",  "admin123", True),
    ("user@test.com",   "user123",  True),
    ("wrong@test.com",  "badpass",  False),
    ("",                "",         False),
])
def test_login_matrix(page, email, password, expected):
    page.goto("/login")
    page.fill("#email", email)
    page.fill("#password", password)
    page.click("#submit")
    if expected:
        assert page.url == "/dashboard"
    else:
        assert page.locator(".error").is_visible()

# ── Run only smoke tests ──
# pytest -m smoke
# pytest -m "not slow"
# pytest -m "smoke and regression"
parametrize output
$ pytest tests/test_login.py -v

PASSED test_login_matrix[admin@test.com-admin123-True]
PASSED test_login_matrix[user@test.com-user123-True]
PASSED test_login_matrix[wrong@test.com-badpass-False]
PASSED test_login_matrix[-empty-False]

4 passed in 2.34s

1.7 — conftest.py

conftest.py is a special pytest file for shared fixtures. It's auto-discovered — no import needed. Fixtures defined here are available to all tests in the same directory and below.

conftest.py, paylaşılan fixture'lar için özel bir pytest dosyasıdır. Otomatik keşfedilir — import gerekmiyor. Burada tanımlanan fixture'lar, aynı dizin ve alt dizinlerdeki tüm testler için kullanılabilir.

python — tests/conftest.py
import pytest
import os
from playwright.sync_api import sync_playwright

# ── Browser / Page ──
@pytest.fixture(scope="session")
def browser_context_args(browser_context_args):
    return {
        **browser_context_args,
        "base_url": os.getenv("BASE_URL", "http://localhost:3000"),
        "viewport": {"width": 1280, "height": 720},
        "record_video_dir": "videos/" if os.getenv("RECORD") else None,
    }

# ── Auth fixture ──
@pytest.fixture(scope="session")
def auth_token():
    """Get JWT once for all tests — expensive API call."""
    import requests
    resp = requests.post("/api/auth/login", json={
        "email": os.getenv("TEST_EMAIL"),
        "password": os.getenv("TEST_PASS"),
    })
    return resp.json()["token"]

# ── Screenshot on failure ──
@pytest.fixture(autouse=True)
def screenshot_on_fail(request, page):
    yield
    if request.node.rep_call.failed:
        page.screenshot(path=f"screenshots/{request.node.name}.png")

# ── Hook: mark test result ──
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
    outcome = yield
    rep = outcome.get_result()
    setattr(item, f"rep_{rep.when}", rep)

1.8 — Running TestsTest Çalıştırma

bash
# Basic run
pytest

# Verbose output
pytest -v

# Run specific file
pytest tests/test_login.py

# Run specific test
pytest tests/test_login.py::test_valid_login

# Run by marker
pytest -m smoke
pytest -m "not slow"

# Stop after first failure
pytest -x

# Parallel execution (pytest-xdist)
pytest -n 4          # 4 workers
pytest -n auto       # auto-detect CPU count

# HTML report (pytest-html)
pytest --html=report.html --self-contained-html

# Coverage report
pytest --cov=src --cov-report=html

# Allure report
pytest --alluredir=allure-results
allure serve allure-results
Full test run
$ pytest -v --tb=short -m smoke

=================== test session starts ===================
platform linux -- Python 3.11.4, pytest-7.4.0
collected 12 items / 8 deselected / 4 selected

PASSED tests/test_login.py::test_homepage_loads [0.12s]
PASSED tests/test_login.py::test_valid_login [0.34s]
FAILED tests/test_checkout.py::test_add_to_cart [1.23s]
XFAIL tests/test_payment.py::test_expired_card [0.08s]

─────────────── short test summary ────────────────
FAILED tests/test_checkout.py::test_add_to_cart
AssertionError: Expected cart count 1, got 0

========= 1 failed, 2 passed, 1 xfailed in 1.81s =========

Framework ComparisonFramework Karşılaştırması

pytest (Python)
95
JUnit 5 (Java)
85
unittest (Python)
55
nose2 (Python)
35

Scores based on: ecosystem, fixture system, parametrize support, reporting, plugin countPuanlar şuna göre: ekosistem, fixture sistemi, parametrize desteği, raporlama, plugin sayısı

2.1 — What is Selenium?Selenium Nedir?

Selenium WebDriver is the gold standard for cross-browser UI automation since 2004. It controls a real browser via the WebDriver protocol — exactly what a user would do. Python's selenium library talks to ChromeDriver/GeckoDriver to send commands.

Selenium WebDriver, 2004'ten beri çapraz tarayıcı UI otomasyonunun altın standardıdır. WebDriver protokolü aracılığıyla gerçek bir tarayıcıyı kontrol eder — tıpkı bir kullanıcının yapacağı gibi. Python'ın selenium kütüphanesi, komutlar göndermek için ChromeDriver/GeckoDriver ile iletişim kurar.

🌍

Cross-Browser

Chrome, Firefox, Safari, Edge — same Python code, different driver.Chrome, Firefox, Safari, Edge — aynı Python kodu, farklı driver.

🏛️

Industry StandardEndüstri Standardı

Most QA teams know Selenium. Huge community, tons of resources.Çoğu QA ekibi Selenium bilir. Büyük topluluk, bol kaynak.

🔗

pytest Integration

Use pytest-selenium or manage WebDriver as a pytest fixture.pytest-selenium kullan ya da WebDriver'ı pytest fixture olarak yönet.

📦

Selenium 4

BiDi protocol, relative locators, native Chrome DevTools support.BiDi protokolü, relative locator'lar, yerel Chrome DevTools desteği.

ArchitectureMimari

Selenium WebDriver Architecture
test_login.py Python test WebDriver API selenium.webdriver Python library HTTP/W3C chromedriver Driver binary DevTools Chrome Real browser

2.2 — SetupKurulum

bash
pip install selenium webdriver-manager pytest
python — conftest.py
import pytest
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager

@pytest.fixture(scope="function")
def driver():
    options = webdriver.ChromeOptions()
    options.add_argument("--headless")
    options.add_argument("--window-size=1280,720")
    options.add_argument("--no-sandbox")

    service = Service(ChromeDriverManager().install())
    drv = webdriver.Chrome(service=service, options=options)
    drv.implicitly_wait(10)  # default wait
    yield drv
    drv.quit()               # always closes browser

2.3 — Locator StrategiesLocator Stratejileri

Selenium can find elements 8 ways. Prefer ID > CSS > XPath in that order for stability.

Selenium, elementleri 8 farklı şekilde bulabilir. Kararlılık için sırasıyla ID > CSS > XPath tercih edin.

🆔 ID FastestEn Hızlı
🎨 CSS Selector
🔖 XPath
🏷️ Name
🔗 Link Text
📌 Partial Link Text
🏗️ Tag Name
📍 Class Name
python
from selenium.webdriver.common.by import By

# ── Preferred order ──
el = driver.find_element(By.ID, "submit-btn")         # ✅ Best: unique, fast
el = driver.find_element(By.CSS_SELECTOR, "#submit")  # ✅ Good: CSS power
el = driver.find_element(By.CSS_SELECTOR, ".form .btn[type='submit']")
el = driver.find_element(By.XPATH, "//button[@data-testid='submit']")

# ── Multiple elements ──
items = driver.find_elements(By.CSS_SELECTOR, "li.product")
count = len(items)

# ── Relative Locators (Selenium 4) ──
from selenium.webdriver.support.relative_locator import locate_with

password_label = driver.find_element(By.ID, "password-label")
password_field = driver.find_element(
    locate_with(By.TAG_NAME, "input").to_right_of(password_label)
)

# ── Bad practices to avoid ──
# ❌ driver.find_element(By.XPATH, "/html/body/div[3]/form/input[2]")
# ❌ driver.find_element(By.CLASS_NAME, "btn")  # class names change

Visual: Finding an ElementGörsel: Eleman Bulma

https://example.com/login
Login admin@test.com •••••••• Submit By.ID, "submit-btn" .click()

2.4 — Browser ActionsTarayıcı Eylemleri

🖱️ click()
⌨️ send_keys()
🗑️ clear()
📜 scroll
🖱️🖱️ double_click
🖱️➡️ drag_and_drop
📸 screenshot
⬅️➡️ navigate
🪟 switch_to
💉 execute_script
python
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.keys import Keys

# ── Basic actions ──
driver.get("https://example.com/login")
driver.find_element(By.ID, "email").send_keys("admin@test.com")
driver.find_element(By.ID, "password").send_keys("secret")
driver.find_element(By.ID, "submit").click()

# ── Keyboard shortcuts ──
field = driver.find_element(By.ID, "search")
field.send_keys("pytest")
field.send_keys(Keys.ENTER)                       # press Enter
field.send_keys(Keys.CONTROL + "a")               # Ctrl+A

# ── Advanced: ActionChains ──
actions = ActionChains(driver)

# Hover over element
menu = driver.find_element(By.ID, "nav-menu")
actions.move_to_element(menu).perform()

# Drag and drop
source = driver.find_element(By.ID, "drag-item")
target = driver.find_element(By.ID, "drop-zone")
actions.drag_and_drop(source, target).perform()

# Double click
actions.double_click(driver.find_element(By.ID, "item")).perform()

# ── JavaScript execution ──
driver.execute_script("window.scrollTo(0, document.body.scrollHeight)")
driver.execute_script("arguments[0].click();", element)  # bypass overlays

# ── Screenshots ──
driver.save_screenshot("screenshots/login_page.png")
element.screenshot("screenshots/button.png")  # element-level

2.5 — WaitsBekleme Stratejileri

Flaky tests almost always come from timing issues. Never use time.sleep() — use explicit waits.

Kararsız testler neredeyse her zaman zamanlama sorunlarından kaynaklanır. time.sleep() asla kullanmayın — explicit wait kullanın.

❌ Implicit Wait
Set once globally. WebDriver polls for N seconds before throwing NoSuchElement. Applies to ALL find_element calls — causes slow failures.Bir kez global olarak ayarlanır. WebDriver, NoSuchElement hatası vermeden önce N saniye boyunca yoklar. Tüm find_element çağrılarına uygulanır — yavaş hatalara neden olur.
driver.implicitly_wait(10)
✅ Explicit Wait
Wait for a specific condition on a specific element. Most reliable. Use WebDriverWait with expected_conditions.Belirli bir koşulun belirli bir elemanda gerçekleşmesini bekle. En güvenilir yöntem. WebDriverWait'i expected_conditions ile kullan.
WebDriverWait(driver, 10).until(EC.clickable)
⚠️ Fluent Wait
Explicit wait with custom poll interval and ignored exceptions. Useful for slow AJAX but adds complexity. Prefer explicit wait for most cases.Özel poll aralığı ve yoksayılan exception'larla explicit wait. Yavaş AJAX için kullanışlı ama karmaşıklık katar. Çoğu durumda explicit wait tercih edin.
WebDriverWait(driver,10,poll_frequency=.5)
python
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By

wait = WebDriverWait(driver, timeout=10)

# ── Most common conditions ──
el = wait.until(EC.presence_of_element_located((By.ID, "modal")))
el = wait.until(EC.visibility_of_element_located((By.ID, "result")))
el = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "#submit")))
wait.until(EC.url_contains("/dashboard"))
wait.until(EC.title_is("Dashboard"))
wait.until(EC.text_to_be_present_in_element((By.ID, "msg"), "Success"))

# ── Wait for element to DISAPPEAR ──
wait.until(EC.invisibility_of_element_located((By.ID, "loading-spinner")))

# ── Custom wait condition ──
def element_has_class(locator, cls):
    def check(driver):
        el = driver.find_element(*locator)
        return cls in el.get_attribute("class")
    return check

wait.until(element_has_class((By.ID, "btn"), "active"))

2.6 — Page Object Model

POM separates locators and actions from test logic. One class per page. Tests become readable business flows — not brittle locator soup.

POM, locator'ları ve aksiyonları test mantığından ayırır. Sayfa başına bir sınıf. Testler, kırılgan locator karmaşası yerine okunabilir iş akışlarına dönüşür.

☕ Java POM vs Python POM — Same Pattern, Cleaner SyntaxAynı Desen, Daha Temiz Sözdizimi
☕ Java
public class LoginPage {
    private WebDriver driver;

    @FindBy(id = "email")
    private WebElement emailField;

    @FindBy(id = "submit")
    private WebElement submitBtn;

    public LoginPage(WebDriver driver) {
        this.driver = driver;
        PageFactory.initElements(driver, this);
    }

    public void login(String email, String pass) {
        emailField.sendKeys(email);
        driver.findElement(By.id("password"))
              .sendKeys(pass);
        submitBtn.click();
    }
}
🐍 Python
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:
    EMAIL = (By.ID, "email")
    PASSWORD = (By.ID, "password")
    SUBMIT = (By.CSS_SELECTOR, "#submit")

    def __init__(self, driver):
        self.driver = driver
        self.wait = WebDriverWait(driver, 10)

    def login(self, email: str, password: str):
        self.wait.until(EC.element_to_be_clickable(
            self.EMAIL)).send_keys(email)
        self.driver.find_element(*self.PASSWORD).send_keys(password)
        self.driver.find_element(*self.SUBMIT).click()
        return DashboardPage(self.driver)  # chain pages
💡 Python POM uses tuples for locators (By.ID, "email") instead of annotations. The * unpacks the tuple: find_element(*self.EMAIL) = find_element(By.ID, "email")💡 Python POM, annotation yerine locator'lar için tuple kullanır (By.ID, "email"). * tuple'ı açar: find_element(*self.EMAIL) = find_element(By.ID, "email")
python — tests/test_login.py
def test_valid_login(driver):
    login_page = LoginPage(driver)
    driver.get("https://example.com/login")

    dashboard = login_page.login("admin@test.com", "secret")

    assert dashboard.get_welcome_message() == "Welcome, Admin!"
    assert dashboard.is_logged_in()

def test_invalid_login(driver):
    login_page = LoginPage(driver)
    driver.get("https://example.com/login")
    login_page.login("wrong@test.com", "badpass")

    assert login_page.get_error_message() == "Invalid credentials"

3.1 — What is Playwright?Playwright Nedir?

Playwright is Microsoft's modern browser automation library (2020). Unlike Selenium's HTTP-based WebDriver, Playwright uses a direct WebSocket connection to browser DevTools — making it dramatically faster and more reliable, with built-in auto-waiting.

Playwright, Microsoft'un modern tarayıcı otomasyon kütüphanesidir (2020). Selenium'un HTTP tabanlı WebDriver'ından farklı olarak, Playwright tarayıcı DevTools'a doğrudan WebSocket bağlantısı kullanır — bu da onu dramatik biçimde daha hızlı ve güvenilir kılar, yerleşik auto-wait ile.

⚡ Auto-Wait

Every action waits automatically — no explicit waits needed for most cases.Her eylem otomatik olarak bekler — çoğu durumda explicit wait gerekmez.

🌐 Cross-Browser

Chromium, Firefox, and WebKit (Safari) — all bundled, no driver management.Chromium, Firefox ve WebKit (Safari) — hepsi dahil, driver yönetimi yok.

📸 Trace Viewer

Record a trace zip with screenshots, network, console, DOM snapshots.Ekran görüntüleri, network, konsol ve DOM snapshot'larıyla trace kaydı alın.

🕸️ Network ControlAğ Kontrolü

Intercept, mock, block any HTTP request. API mocking built-in.Herhangi bir HTTP isteğini yakala, mock'la veya engelle. API mocking yerleşik.

📱 Mobile EmulationMobil Emülasyon

Emulate any device with correct viewport, touch events, user agent.Doğru viewport, dokunma olayları ve user agent ile herhangi bir cihazı emüle edin.

🎭 Multiple ContextsÇoklu Context'ler

Isolated browser contexts = fast parallel tests with different auth states.İzole browser context'leri = farklı auth durumlarıyla hızlı paralel testler.

Playwright vs SeleniumPlaywright ile Selenium Karşılaştırması

FeatureÖzellik🎭 Playwright🌐 Selenium
Auto-waitingOtomatik Bekleme Built-inYerleşik Manual waitsManuel bekleme
Driver managementDriver Yönetimi Bundled browsersDahili tarayıcılar⚠️ webdriver-manager
SpeedHız Faster (DevTools)Daha hızlı (DevTools)⚠️ Slower (HTTP)Daha yavaş (HTTP)
Network mockingAğ Mocklaması NativeYerel Proxy requiredProxy gerekli
Parallel testsParalel Testler Browser contexts⚠️ Multiple processesBirden fazla süreç
Industry adoptionEndüstri Benimsemesi⚠️ Growing fastHızla büyüyor Dominant (legacy)Baskın (eski)
Mobile emulationMobil Emülasyon Built-in devicesDahili cihazlar⚠️ LimitedSınırlı
Trace / debuggingİzleme / Hata Ayıklama Trace Viewer Screenshots onlyYalnızca ekran görüntüsü

3.2 — SetupKurulum

bash
pip install playwright pytest-playwright
playwright install               # downloads Chromium, Firefox, WebKit
playwright install chromium      # or just one browser
ini — pytest.ini
[pytest]
addopts = --browser chromium --headed
base_url = https://example.com

3.3 — First Testİlk Test

☕ Java Playwright vs Python Playwright — ComparisonKarşılaştırma
☕ Java (Playwright for Java)
import com.microsoft.playwright.*;
import com.microsoft.playwright.assertions.*;
import org.junit.jupiter.api.*;

class LoginTest {
    Playwright pw;
    Browser browser;
    Page page;

    @BeforeEach void setup() {
        pw = Playwright.create();
        browser = pw.chromium().launch();
        page = browser.newPage();
    }

    @Test void testLogin() {
        page.navigate("https://example.com/login");
        page.fill("#email", "admin@test.com");
        page.fill("#password", "secret");
        page.click("#submit");
        PlaywrightAssertions
            .assertThat(page)
            .hasURL("/dashboard");
    }

    @AfterEach void tearDown() {
        browser.close(); pw.close();
    }
}
🐍 Python (pytest-playwright)
from playwright.sync_api import Page, expect

def test_login(page: Page):
    # No setup! pytest-playwright handles it
    page.goto("/login")
    page.fill("#email", "admin@test.com")
    page.fill("#password", "secret")
    page.click("#submit")

    # Built-in assertions:
    expect(page).to_have_url("/dashboard")
    expect(page.locator("h1")).to_have_text("Welcome!")

# Python needs ZERO setup code.
# pytest-playwright injects 'page' fixture automatically.
# Browser opens, test runs, browser closes — hands-free.
💡 Python Playwright uses sync_api (no async/await needed). Java requires @BeforeEach/@AfterEach boilerplate; Python handles it via the built-in page fixture from pytest-playwright.💡 Python Playwright, sync_api kullanır (async/await gerekmez). Java, @BeforeEach/@AfterEach şablonu gerektirir; Python bunu pytest-playwright'ın yerleşik page fixture'ı aracılığıyla halleder.
🎯
expect() vs assert Playwright's expect(locator).to_have_text("x") retries the assertion for up to 5 seconds — it's an assertion WITH auto-wait built in. Plain assert checks once and fails immediately. Playwright'ın expect(locator).to_have_text("x") ifadesi 5 saniyeye kadar yeniden dener — yerleşik auto-wait içeren bir doğrulamadır. Düz assert ise bir kez kontrol edip hemen hata verir.

3.4 — Auto-Wait MechanismAuto-Wait Mekanizması

Before every action, Playwright checks that the element is attached → visible → stable → enabled → not covered. No WebDriverWait needed for normal interactions.

Her eylemden önce, Playwright elementin attached → visible → stable → enabled → not covered olduğunu kontrol eder. Normal etkileşimler için WebDriverWait gerekmez.

python
from playwright.sync_api import Page, expect

def test_checkout_flow(page: Page):
    page.goto("/shop")

    # ── Playwright auto-waits before each action ──
    page.click(".add-to-cart")         # waits until button is clickable
    page.click("#cart-icon")           # waits until visible
    page.click("text=Proceed to Pay")  # text locator!

    page.fill("#card-number", "4111111111111111")
    page.select_option("#expiry-month", "12")
    page.click("#pay-btn")

    # ── Assertions with built-in retry ──
    expect(page.locator(".order-confirmation")).to_be_visible()
    expect(page.locator(".order-id")).to_have_text(r"ORD-\d{6}")

    # ── Only use explicit waits for tricky cases ──
    page.wait_for_url("**/confirmation")
    page.wait_for_selector(".success-msg", state="visible")
    page.wait_for_load_state("networkidle")

    # ── When you NEED to wait for an element to be GONE ──
    page.wait_for_selector("#loading-spinner", state="hidden")
Playwright Auto-Wait Chain (runs before every action) Attached Visible Stable Enabled Not covered → Action! ✅ Timeout 30s by default — configurable per-action

3.5 — Network & API Mocking

python
from playwright.sync_api import Page, Route
import json

def test_with_mocked_api(page: Page):
    """Intercept API call and return fake data — no real backend needed."""

    def handle_users(route: Route):
        route.fulfill(
            status=200,
            content_type="application/json",
            body=json.dumps([
                {"id": 1, "name": "Alice", "role": "admin"},
                {"id": 2, "name": "Bob",   "role": "user"},
            ])
        )

    page.route("**/api/users", handle_users)
    page.goto("/users")

    expect(page.locator(".user-row")).to_have_count(2)
    expect(page.locator(".user-row").first).to_contain_text("Alice")

def test_block_analytics(page: Page):
    """Block tracking/analytics requests to speed up tests."""
    page.route("**/*google-analytics*", lambda r: r.abort())
    page.route("**/*hotjar*", lambda r: r.abort())
    page.goto("/")

def test_api_directly(page: Page):
    """Use Playwright's APIRequestContext — no requests library needed."""
    resp = page.request.post(
        "https://api.example.com/login",
        data={"email": "admin@test.com", "password": "secret"},
    )
    assert resp.ok
    token = resp.json()["token"]
    assert token.startswith("eyJ")
💡
Combined UI + API TestingUI + API Testi Birleştirme Use page.request to set up test data via API, then do UI assertions. This is 10x faster than navigating through the UI for setup steps. Test verilerini API üzerinden kurmak için page.request kullan, ardından UI doğrulamaları yap. Bu, setup adımları için UI üzerinden gezinmekten 10 kat daha hızlıdır.

3.6 — Page Object Model

python — pages/login_page.py
from playwright.sync_api import Page, Locator, expect

class LoginPage:
    # ── Locators as properties ──
    def __init__(self, page: Page):
        self.page = page
        self.email: Locator    = page.locator("#email")
        self.password: Locator = page.locator("#password")
        self.submit: Locator   = page.get_by_role("button", name="Sign In")
        self.error: Locator    = page.locator(".error-message")

    def navigate(self):
        self.page.goto("/login")

    def login(self, email: str, password: str) -> "DashboardPage":
        self.email.fill(email)
        self.password.fill(password)
        self.submit.click()
        return DashboardPage(self.page)

    def expect_error(self, message: str):
        expect(self.error).to_be_visible()
        expect(self.error).to_have_text(message)


class DashboardPage:
    def __init__(self, page: Page):
        self.page = page
        self.welcome = page.locator("h1.welcome")
        self.logout_btn = page.get_by_role("button", name="Logout")

    def expect_loaded(self):
        expect(self.page).to_have_url("/dashboard")
        expect(self.welcome).to_be_visible()


# ── Test using POM ──
def test_login_success(page: Page):
    lp = LoginPage(page)
    lp.navigate()
    dashboard = lp.login("admin@test.com", "secret")
    dashboard.expect_loaded()

def test_login_invalid(page: Page):
    lp = LoginPage(page)
    lp.navigate()
    lp.login("wrong@example.com", "bad")
    lp.expect_error("Invalid credentials")
get_by_role vs locator Prefer get_by_role("button", name="Sign In"), get_by_label("Email"), get_by_placeholder("Enter email") — these match the accessibility tree and are more resilient to CSS/DOM changes than ID or CSS selectors. get_by_role("button", name="Sign In"), get_by_label("Email"), get_by_placeholder("Enter email") tercih et — bunlar erişilebilirlik ağacıyla eşleşir ve ID/CSS selector'lara göre CSS/DOM değişikliklerine karşı daha dayanıklıdır.

Framework Selection GuideFramework Seçim Rehberi

Use CaseKullanım SenaryosuRecommendationÖneriWhyNeden
New project, modern stackYeni proje, modern stack🎭 PlaywrightAuto-wait, faster, better DXOtomatik bekleme, daha hızlı, daha iyi DX
Legacy project (Selenium already in use)Eski proje (Selenium zaten kullanımda)🌐 SeleniumMigration cost not worth itGeçiş maliyeti buna değmez
Cross-browser incl. SafariSafari dahil çoklu tarayıcı🎭 PlaywrightWebKit bundled, no extra setupWebKit dahili, ekstra kurulum yok
Complex test data setupKarmaşık test verisi kurulumu🧪 pytest + APIpytest fixtures + requestspytest fixture'ları + requests
API-only testingYalnızca API testi🧪 pytest + requestsNo browser overhead neededTarayıcı yükü gerekmez
Enterprise / Java team uses SeleniumKurumsal / Java ekibi Selenium kullanıyor🌐 SeleniumShared knowledge, toolingOrtak bilgi ve araçlar