Araç seçimi neden hâlâ yanlış kriterlerle yapılıyor
Yeni projede araç seçimi çoğu zaman teknik bir karar gibi görünür ama pratikte üç şeyle bitirilir: birinin syntax beğenisi, geçen ay okunan bir blog yazısı ve “ekip zaten biliyor” cümlesi. Bu üçü de kararı iki yıl sonra ödeyeceğin faturayı hesaba katmaz. Fatura CI dakikası, flaky test bakımı ve ekibe yeni giren kişinin ilk testini yazana kadar geçen süredir.
Seçimi gerçekten belirleyen üç ölçüt var: test edilecek uygulamanın mimarisi (kaç origin, kaç sekme, iframe var mı, OAuth yönlendirmesi var mı), CI bütçesi (paralel çalıştırmayı kim ödüyor, altyapıyı kim bakıyor) ve ekibin dil yetkinliği (testleri kim yazacak, kim gözden geçirecek). Aşağıdaki karşılaştırmanın tamamı bu üç eksende kurulu. Genel bir araç envanteri arıyorsan test otomasyonunda kullanılan popüler araçlar yazısı başlangıç noktası; burada üç aracı karar verecek kadar derin karşılaştırıyoruz.
2026’da üç aracın gerçek durumu
Playwright artık “deneyeceğimiz yeni araç” kategorisinde değil. Microsoft desteği, TypeScript/JavaScript, Python, Java ve .NET bağlamaları, kutudan çıkan trace viewer, HTML raporu, otomatik bekleme ve test runner’ı ile varsayılan tercih. Çözmek üzere tasarlandığı problem şu: modern tarayıcıyı dış taraftan, protokol seviyesinde, tek bağlantı üzerinden sürmek ve bunu yaparken ağ katmanını da eline almak.
Cypress frontend geliştiricisinin kendi uygulamasını kendi tarayıcısında hata ayıklaması için tasarlandı ve bu problemi hâlâ en iyi o çözüyor. Time-travel debugger ve component test entegrasyonu güçlü. Karşılığında tarayıcı içinde çalışmanın yapısal kısıtlarını taşıyor ve şirket ticari orkestrasyon tarafına ağırlık verdi — paralel çalıştırmayı ücretsiz istiyorsan üçüncü parti çözüme gidersin.
Selenium bir standart olarak varlığını sürdürüyor. WebDriver BiDi’nin olgunlaşmasıyla ağ dinleme ve konsol log erişimi gibi eksikleri kapanıyor; Grid ile geniş tarayıcı matrisi kurmak hâlâ en sağlam yol. Çözdüğü problem: tek bir API ile mümkün olan her tarayıcıyı, her dilden, her işletim sisteminde sürmek. Kurumsal regresyon ve legacy tarayıcı matrisi varsa rakibi yok.
Mimari fark: aynı tarayıcıya üç farklı bağlanma yolu
Üç aracın özellik listesindeki farklar tercihlerden değil, mimariden türüyor. Cypress test kodunu tarayıcı sekmesinin içinde, uygulamayla aynı JavaScript ortamında çalıştırır. Playwright testi Node sürecinde tutar, tarayıcıya tek bir WebSocket üzerinden CDP (Chromium) veya BiDi ile bağlanır. Selenium test kodundan sürücü ikilisine (chromedriver vb.) HTTP isteği gönderir, sürücü tarayıcıyı sürer.

Sonuçları: Cypress uygulamayla aynı origin’e hapsolduğu için çok origin ve çok sekme senaryolarında kenar çözümlere muhtaç, buna karşılık ağ isteklerini doğal olarak görür ve hata ayıklaması eşsizdir. Playwright dışarıdan bağlandığı için sekme, pencere, indirme, iframe ve origin sınırı tanımaz; tek bağlantı komut gecikmesini düşük tutar. Selenium her komut için sürücü katmanından geçer — bu ek gidiş-dönüş maliyeti demektir ama aynı katman Safari’den eski Edge’e kadar her şeyi aynı API’nin arkasına koyar.
Karşılaştırma tablosu: on üç somut kriter
| Kriter | Playwright | Cypress | Selenium |
|---|---|---|---|
| Dil desteği | TS/JS, Python, Java, .NET | Yalnız JS/TS | Java, C#, Python, JS, Ruby ve daha fazlası |
| Tarayıcı matrisi | Chromium, Firefox, WebKit (gerçek Safari değil) | Chrome ailesi, Firefox, WebKit deneysel | Gerçek Safari, Edge, uzak/legacy dahil her şey |
| Paralel çalıştırma maliyeti | Worker + shard, ek ücret yok | Ücretsiz orkestrasyon yok; Cloud veya üçüncü parti | Grid ücretsiz, altyapı bakımı sana ait |
| Otomatik bekleme | Locator bazlı, her eylemde actionability kontrolü | Komut zincirinde retry, should ile birlikte | Yok; WebDriverWait elle kurulur |
| Ağ mocklama | route ile tam kontrol, HAR kaydı/oynatma | intercept olgun ve rahat | BiDi/CDP ile mümkün, API daha kaba |
| Çok origin / çok sekme | Doğal; context, popup, newPage | Kısıtlı; cy.origin ile kısmen | Doğal; pencere handle’ları |
| Mobil web | Cihaz emülasyonu var, gerçek cihaz yok | Viewport emülasyonu, cihaz yok | Appium ile gerçek cihaz |
| iframe / shadow DOM | frameLocator, shadow DOM şeffaf | iframe zahmetli, shadow için eklenti | Frame geçişi elle, shadow için getShadowRoot |
| Trace ve görsel hata ayıklama | Trace viewer: DOM anlık görüntüsü, ağ, konsol | Time-travel debugger, en iyi yerel deneyim | Ekran görüntüsü + log; gerisi sana kalmış |
| Raporlama | Yerleşik HTML rapor, JUnit/JSON çıktı | Yerel rapor sade; zengini Cloud’da | Kütüphane dışı; Allure/TestNG eklenir |
| Component test | Deneysel etikette, React/Vue/Svelte | Olgun ve birinci sınıf | Yok |
| Ekosistem | Hızlı büyüyor, çekirdek zaten dolu | Eklenti kültürü güçlü | En geniş, en eski, bazısı bakımsız |
| Öğrenme eğrisi | Orta; async/await ve locator mantığı | Düşük (JS bilen için), zincir mantığı alışkanlık ister | Yüksek; bekleme ve sürücü yönetimi elle |
Tablodaki en pahalı üç satır şunlar: paralel çalıştırma maliyeti, otomatik bekleme ve trace. İlki CI faturana, ikincisi flaky test sayına, üçüncüsü bir hatanın kök nedenini bulma sürene doğrudan yansır.
Aynı senaryo, üç araçta kod
Senaryo: giriş yap, kullanıcı tablosunda ara, aramayı tetikleyen API yanıtını yakala ve hem yanıtı hem ekranı doğrula.
Playwright (TypeScript)
import { test, expect } from '@playwright/test';
test('arama sonucu API ve ekranda tutarlı', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('E-posta').fill('**@*****le.com');
await page.getByLabel('Parola').fill('S3cret!');
await page.getByRole('button', { name: 'Giriş yap' }).click();
await expect(page.getByRole('heading', { name: 'Kullanıcılar' })).toBeVisible();
const responsePromise = page.waitForResponse(
(r) => r.url().includes('/api/users') && r.url().includes('q=ayse') && r.ok()
);
await page.getByPlaceholder('Ara').fill('ayse');
const body = await (await responsePromise).json();
expect(body.items).toHaveLength(1);
expect(body.items[0].fullName).toBe('Ayşe Yılmaz');
await expect(page.getByRole('row')).toHaveCount(2); // başlık + 1 sonuç
await expect(page.getByRole('cell', { name: 'Ayşe Yılmaz' })).toBeVisible();
});
Cypress (JavaScript)
describe('kullanıcı arama', () => {
it('API ve ekran tutarlı', () => {
cy.intercept('GET', '**/api/users?*q=ayse*').as('arama');
cy.visit('/login');
cy.get('input[name="email"]').type('**@*****le.com');
cy.get('input[name="password"]').type('S3cret!');
cy.contains('button', 'Giriş yap').click();
cy.contains('h1', 'Kullanıcılar').should('be.visible');
cy.get('input[placeholder="Ara"]').type('ayse');
cy.wait('@arama').then(({ response }) => {
expect(response.statusCode).to.eq(200);
expect(response.body.items).to.have.length(1);
expect(response.body.items[0].fullName).to.eq('Ayşe Yılmaz');
});
cy.get('table tbody tr').should('have.length', 1);
cy.contains('table tbody td', 'Ayşe Yılmaz').should('be.visible');
});
});
Selenium (Java 17, Selenium 4, Jackson)
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.time.Duration;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.jupiter.api.*;
import org.openqa.selenium.*;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.devtools.NetworkInterceptor;
import org.openqa.selenium.remote.http.*;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
import static org.junit.jupiter.api.Assertions.*;
class KullaniciAramaTest {
private ChromeDriver driver;
private WebDriverWait wait;
private NetworkInterceptor interceptor;
private final AtomicReference<String> yakalanan = new AtomicReference<>();
@BeforeEach
void setUp() {
driver = new ChromeDriver();
wait = new WebDriverWait(driver, Duration.ofSeconds(10));
// Not: yanıt gzip ile sıkıştırılmışsa Accept-Encoding'i identity'ye zorlamak gerekir.
interceptor = new NetworkInterceptor(driver, (Filter) next -> req -> {
HttpResponse res = next.execute(req);
if (req.getUri().contains("/api/users") && req.getUri().contains("q=ayse")) {
String govde = Contents.string(res);
yakalanan.set(govde);
HttpResponse kopya = new HttpResponse()
.setStatus(res.getStatus())
.setContent(Contents.utf8String(govde));
res.getHeaderNames().forEach(n -> res.getHeaders(n).forEach(v -> kopya.addHeader(n, v)));
return kopya;
}
return res;
});
}
@Test
void aramaSonucuApiVeEkrandaTutarli() throws Exception {
driver.get("https://uygulama.example.com/login");
driver.findElement(By.name("email")).sendKeys("**@*****le.com");
driver.findElement(By.name("password")).sendKeys("S3cret!");
driver.findElement(By.cssSelector("button[type='submit']")).click();
wait.until(ExpectedConditions.textToBePresentInElementLocated(By.tagName("h1"), "Kullanıcılar"));
driver.findElement(By.cssSelector("input[placeholder='Ara']")).sendKeys("ayse");
wait.until(d -> yakalanan.get() != null);
JsonNode body = new ObjectMapper().readTree(yakalanan.get());
assertEquals(1, body.get("items").size());
assertEquals("Ayşe Yılmaz", body.get("items").get(0).get("fullName").asText());
wait.until(ExpectedConditions.numberOfElementsToBe(By.cssSelector("table tbody tr"), 1));
assertTrue(driver.findElement(By.cssSelector("table tbody td")).getText().contains("Ayşe Yılmaz"));
}
@AfterEach
void tearDown() {
if (interceptor != null) interceptor.close();
if (driver != null) driver.quit();
}
}
Fark tabloda okunandan daha net görünüyor: Playwright ve Cypress’te ağ katmanı tek satır, Selenium’da yanıtı yakalamak için proxy filtresi kurup gövdeyi yeniden inşa etmen gerekiyor. Bekleme tarafında Playwright ve Cypress varsayılan olarak retry yaparken Selenium’da her doğrulamanın beklemesini sen yazıyorsun — ve yazmadığın her yer ileride flaky test adayı.
CI maliyeti ve çalıştırma süresi
Aracın en somut sonucu aylık CI faturasıdır. Playwright’ta paralellik iki katmanlı: süreç içinde worker, süreçler arası shard. İkisi de ücretsiz.
# .github/workflows/e2e.yml
name: e2e
on: [push]
jobs:
test:
runs-on: ubuntu-latest
container: mcr.microsoft.com/playwright:v1.49.0-noble
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx playwright test --shard=${{ matrix.shard }}/4 --workers=2
- uses: actions/upload-artifact@v4
if: failure()
with:
name: trace-${{ matrix.shard }}
path: test-results/
retention-days: 7
Cypress’te aynı bölüşmeyi ücretsiz yapmak için ya spec listesini elle paylaştırırsın ya da üçüncü parti orkestratör kullanırsın:
test:
runs-on: ubuntu-latest
container: cypress/included:13.15.0
strategy:
fail-fast: false
matrix:
grup: [a, b]
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx cypress run --spec "cypress/e2e/${{ matrix.grup }}/**/*.cy.js"
Bu yöntemin bedeli dengesiz dağılımdır: bir grup on dakika, diğeri iki dakika sürer ve toplam süre en yavaş grubun süresidir.
Selenium Grid’de ölçek senin elinde ama altyapı da senin:
# docker-compose.yml
services:
hub:
image: selenium/hub:4.27.0
ports: ["4442:4442", "4443:4443", "4444:4444"]
chrome:
image: selenium/node-chrome:4.27.0
shm_size: 2gb
depends_on: [hub]
environment:
- SE_EVENT_BUS_HOST=hub
- SE_EVENT_BUS_PUBLISH_PORT=4442
- SE_EVENT_BUS_SUBSCRIBE_PORT=4443
- SE_NODE_MAX_SESSIONS=4
- SE_NODE_OVERRIDE_MAX_SESSIONS=true
deploy:
replicas: 3
Üç kalem daha hesaba kat: konteyner imajı boyutu (Playwright imajı tarayıcılarla birlikte gelir, çekme süresi cache’lenmezse her koşuya eklenir), soğuk başlatma (Grid’de node’ların hub’a kaydolması saniyeler alır) ve artifact saklama (trace ve video en hızlı büyüyen depolama kalemidir; retention süresini kısa tut). Süre optimizasyonunun araçtan bağımsız kısmı için test otomasyonunda performans optimizasyonu ve ölçeklenebilirlik uygulamaları yazılarına bak.
Kararı verdiren dört senaryo
1. Yeni SPA, küçük ekip, TypeScript. Playwright. Gerekçe: kutudan çıkan trace, ücretsiz paralellik, tek bağımlılıkla test runner + assertion + rapor. Bu profilde başka bir aracı seçmek için özel bir nedene ihtiyacın var.
2. Uzun tarayıcı matrisi zorunlu kurumsal regresyon. Selenium. Gerçek Safari, kurum içi dağıtılmış eski Edge ya da imzalı sertifika gerektiren masaüstü tarayıcılarını kapsaman gerekiyorsa WebDriver hâlâ tek standart yol. Grid bakım maliyetini kabul ediyorsun çünkü alternatif kapsamı daraltmak.
3. Frontend ekibinin sahiplendiği, component testiyle iç içe suite. Cypress savunulabilir. Aynı araçta component ve e2e yazmak, geliştiricinin testi kendi akışında çalıştırması, time-travel ile hata ayıklaması gerçek kazanç. Tek origin’de yaşayan, OAuth yönlendirmesi olmayan uygulamalarda mimari kısıt seni yormaz.
4. Java/C# monolit ve büyük mevcut Selenium yatırımı. Göç etmemek de bir karardır. Çalışan bin testi yeniden yazmanın getirisi çoğu zaman maliyetini karşılamaz. Doğru hamle: yeni akışları yeni araçta yazıp mevcut seti yerinde bakımda tutmak.
Selenium veya Cypress’ten Playwright’a geçiş planı
Toptan yeniden yazma denemeleri tipik olarak aynı yerde tıkanır: göç ekibi eski setin kapsamını bitiremeden yeni özellik akışı üstüne biner, iki suite de yarım kalır. Kademeli sıra şu:
- Yeni testleri yeni araçta yaz. Kural bugünden yürürlüğe girer, eski sete dokunmadan.
- İki suite’i CI’da paralel koştur. Eski set hâlâ merge’ü bloklar, yeni set önce bilgilendirme modunda çalışır; kararlılığı kanıtlanınca bloklayıcı olur.
- En kırılgan 20 testi taşı. Bakım maliyeti en yüksek testler en çok kazandıran testlerdir; sıralamayı son üç ayın flaky ve düzeltme kayıtlarından çıkar.
- Kapsam eşitlendikçe eski setten sil. Taşınan testin eskisi aynı PR’da silinir; yoksa iki set yıllarca yan yana yaşar.
Göçte en çok vakit alan kısım genellikle testlerin kendisi değil, altındaki iki katmandır: locator stratejisi ve test verisi kurulumu. XPath ve CSS zincirlerini birebir çevirmek, eski kırılganlığı yeni araca taşımak demek — rol tabanlı locator ve data-testid düzenine geçmek için doğru an tam olarak bu göçtür. Aynı şekilde paylaşılan sabit fixture veritabanı taşınmamalı; her testin kendi verisini üretmesi kuralı göç sırasında kurulmazsa sonradan hiç kurulmaz. Giriş gibi tekrar eden hazırlıkları UI yerine API üzerinden yapmak da bu aşamada devreye alınmalı; API test otomasyonu yazısındaki yaklaşım burada doğrudan işe yarar.
Seçimi kağıda dökmek
Bu yazıdaki tablo seni iki aday arasına indirir; kararı POC verir. Yapacağın şu: üç ölçütü (mimari uyum, CI maliyeti, ekip dil yetkinliği) kendi projen için 1–5 arası ağırlıklandır, sonra kendi uygulamanın en kritik üç akışını iki adayda da yaz ve iki hafta boyunca gerçek CI’ında günde en az beş kez koştur.
POC bitiminde ölçeceklerin:
- Üç akışın toplam satır sayısı ve dosya sayısı
- Lokal tek koşu süresi ve CI’da paralel koşu süresi
- Koşu başına CI dakikası ve gereken runner sayısı
- Elli koşuda kaç kez kırmızı, bunların kaçı gerçek hata
- Bilerek kırdığın bir testte kök nedeni bulma süresi (trace/video ile)
- Ekipteki en az deneyimli kişinin dördüncü akışı tek başına yazma süresi
- Konteyner imajı çekme süresi ve artifact boyutu
Son maddeyi atlama: aracı seçen sen olsan da onunla beş yıl yaşayacak olan ekip. Ölçümü tabloya yaz, ağırlıklarla çarp, kararı bir sayfada gerekçesiyle sakla. Bir yıl sonra “neden bunu seçmiştik” sorusunun cevabı o sayfada olsun.