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
pip install pytest pytest-html pytest-xdist allure-pytest pytest-cov pytest-mock
@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.
pytest.ini — ConfigurationYapılandırma
[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
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");
}
}
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.
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ü
fixture
def test_*
after yield
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.
# 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ı
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.
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-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
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"
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.
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
# 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
=================== 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ı
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
2.2 — SetupKurulum
pip install selenium webdriver-manager pytest
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.
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
2.4 — Browser ActionsTarayıcı Eylemleri
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.
driver.implicitly_wait(10)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)WebDriverWait(driver,10,poll_frequency=.5)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.
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();
}
}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
(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")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
pip install playwright pytest-playwright
playwright install # downloads Chromium, Firefox, WebKit
playwright install chromium # or just one browser
[pytest]
addopts = --browser chromium --headed
base_url = https://example.com
3.3 — First Testİlk Test
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();
}
}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.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(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.
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")
3.5 — Network & API Mocking
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")
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
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("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 Senaryosu | RecommendationÖneri | WhyNeden |
|---|---|---|
| New project, modern stackYeni proje, modern stack | 🎭 Playwright | Auto-wait, faster, better DXOtomatik bekleme, daha hızlı, daha iyi DX |
| Legacy project (Selenium already in use)Eski proje (Selenium zaten kullanımda) | 🌐 Selenium | Migration cost not worth itGeçiş maliyeti buna değmez |
| Cross-browser incl. SafariSafari dahil çoklu tarayıcı | 🎭 Playwright | WebKit bundled, no extra setupWebKit dahili, ekstra kurulum yok |
| Complex test data setupKarmaşık test verisi kurulumu | 🧪 pytest + API | pytest fixtures + requestspytest fixture'ları + requests |
| API-only testingYalnızca API testi | 🧪 pytest + requests | No browser overhead neededTarayıcı yükü gerekmez |
| Enterprise / Java team uses SeleniumKurumsal / Java ekibi Selenium kullanıyor | 🌐 Selenium | Shared knowledge, toolingOrtak bilgi ve araçlar |