Playwright vs Cypress: Modern Web Uygulamaları için E2E Test Stratejileri
Playwright ve Cypress ile güvenilir, sürdürülebilir E2E suite'leri: framework seçimi, flaky test önleme, CI/CD entegrasyonu ve optimizasyon.
E2E suite’ler her framework’te aynı birkaç sebeple çöküyor: testler arasında paylaşılan state, markup’a bağlanmış selector’ler ve gerçek bir koşulun yerine konan sabit bekleme süreleri. Framework debug deneyimini belirliyor. Mimari ise suite’in altı ay ve bir arayüz yenilemesi sonrasında hâlâ çalışıp çalışmayacağını belirliyor.
Yeni bir suite için Playwright ile başla. Ücretsiz native parallelism, WebKit ve Firefox kapsamı ve trace viewer, tekrar eden üç sıkıntıyı ücretli bir plana ihtiyaç duymadan ortadan kaldırıyor. Cypress ise zaten interaktif runner’ının içinde çalışan ekipler için makul bir seçim olmayı sürdürüyor. Bu karardan sonrası mimari: page object’ler, kararlı data-testid selector’ler, API üzerinden oluşturulan test verisi ve edge case’leri tarayıcının dışında tutan bir piramit.
Framework Seçimi: Playwright vs Cypress#
Mimari Farklar#
Yeni bir suite için güvenli varsayılan Playwright. Cypress ise ekip interaktif runner’ına ücretsiz shard’lardan daha çok değer veriyorsa yerini hak ediyor. Bu kararı belirleyen yetenekler şunlar:
Örnek Testler#
Auto-waiting’i gösteren basit bir Playwright testi:
import { test, expect } from '@playwright/test';
test('kullanıcı satın alma akışını tamamlayabilir', async ({ page }) => {
await page.goto('/products');
// Element actionable olana kadar auto-wait yapar
await page.getByTestId('product-add-to-cart').click();
await page.getByTestId('checkout-button').click();
// Checkout formunu doldur
await page.getByTestId('shipping-name').fill('Ahmet Yılmaz');
await page.getByTestId('shipping-address').fill('Atatürk Cad. No:123');
await page.getByTestId('payment-card').fill('4242424242424242');
await page.getByTestId('place-order').click();
// Web-first assertion auto-retry yapar
await expect(page.getByTestId('order-confirmation')).toBeVisible();
});
Aynı test Cypress’te:
describe('Satın Alma Akışı', () => {
it('kullanıcının satın almayı tamamlamasına izin verir', () => {
cy.visit('/products');
cy.get('[data-testid="product-add-to-cart"]').click();
cy.get('[data-testid="checkout-button"]').click();
cy.get('[data-testid="shipping-name"]').type('Ahmet Yılmaz');
cy.get('[data-testid="shipping-address"]').type('Atatürk Cad. No:123');
cy.get('[data-testid="payment-card"]').type('4242424242424242');
cy.get('[data-testid="place-order"]').click();
cy.get('[data-testid="order-confirmation"]').should('be.visible');
});
});
Her ikisi de aynı amaca ulaşıyor. Playwright’ın avantajı parallel execution’da ortaya çıkıyor; 8 shard ek maliyet olmadan eşzamanlı çalışıyor. Cypress aynı yetenek için Cypress Cloud subscription gerektiriyor.
Page Object Model ile Test Mimarisi#
Page object’ler testleri UI yapısından ayırıyor. Bir button hareket ettiğinde veya class name değiştiğinde, onlarca test yerine bir dosyayı güncelliyorsun.
Modern Page Object Kurgusu#
// page-objects/LoginPage.ts
import { Page, Locator, expect } from '@playwright/test';
export class LoginPage {
readonly page: Page;
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
readonly errorMessage: Locator;
constructor(page: Page) {
this.page = page;
this.emailInput = page.getByTestId('login-email-input');
this.passwordInput = page.getByTestId('login-password-input');
this.submitButton = page.getByTestId('login-submit-button');
this.errorMessage = page.getByTestId('login-error-message');
}
async goto() {
await this.page.goto('/login');
}
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
async expectLoginSuccess() {
await expect(this.page).toHaveURL(/\/dashboard/);
}
async expectLoginError(message: string) {
await expect(this.errorMessage).toContainText(message);
}
}
Testlerde kullanımı:
test('geçerli credential\'lar login\'e izin verir', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@example.com', 'password123');
await loginPage.expectLoginSuccess();
});
test('geçersiz credential\'lar hata gösterir', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@example.com', 'wrongpassword');
await loginPage.expectLoginError('Geçersiz kimlik bilgileri');
});
Selector Kararlılığı#
Test edeceğin elementler için data-testid attribute’ları kullan. Güvenilir bir naming convention: {scope}-{element}-{type}.
<!-- İyi: Stable, açıklayıcı test ID'ler -->
<button data-testid="product-list-add-to-cart-button">Sepete Ekle</button>
<input data-testid="checkout-shipping-name-input" />
<div data-testid="order-confirmation-message">Sipariş başarıyla verildi</div>
<!-- Kaçın: CSS class'lar refactor'larda değişir -->
<button class="btn btn-primary add-cart">Sepete Ekle</button>
Semantic HTML mevcut olduğunda, role-based locator’ları tercih et:
// Daha iyi: Accessible role kullanıyor
await page.getByRole('button', { name: 'Sepete Ekle' }).click();
// İyi: Explicit test ID
await page.getByTestId('add-to-cart-button').click();
// Kırılgan: Implementation-dependent
await page.locator('.product-card > .actions > button:nth-child(1)').click();
API Mocking Stratejileri#
External API’leri mocklamak test isolation ve reliability sağlıyor. Yaklaşım rendering stratejine bağlı.
Playwright’ın Yerleşik Mock’lama Yolu#
Client-side app’ler için page.route() çoğu durumu hallediyor:
test('API fail olduğunda hata gösterir', async ({ page }) => {
// API call'u intercept et ve error döndür
await page.route('**/api/products', route => {
route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ error: 'Internal Server Error' })
});
});
await page.goto('/products');
await expect(page.getByTestId('error-message'))
.toContainText('Ürünler yüklenemedi');
});
MSW ile Kapsamlı Mock’lama#
Mock Service Worker kompleks senaryolar için daha robust bir API sağlıyor:
// mocks/handlers.ts
import { http, HttpResponse } from 'msw';
export const handlers = [
http.get('/api/products', () => {
return HttpResponse.json([
{ id: 1, name: 'Ürün 1', price: 29.99 },
{ id: 2, name: 'Ürün 2', price: 39.99 }
]);
}),
http.post('/api/orders', async () => {
return HttpResponse.json(
{ orderId: '12345', status: 'confirmed' },
{ status: 201 }
);
})
];
Playwright ile entegrasyon:
// tests/msw.setup.ts
test.beforeEach(async ({ page }) => {
// addInitScript sayfanın içinde çalışıyor; orada module import'ları yok.
// Önce setupWorker(...handlers).start() çağrısını bir dosyaya bundle et, sonra enjekte et.
await page.addInitScript({ path: './tests/msw-init.bundle.js' });
});
Gotcha: MSW’nin service worker’ı network request’leri page.route()’a görünmez yapıyor. Bir yaklaşımı tutarlı kullan veya @msw/playwright ile açıkça entegre et.
Flaky Test Önleme#
Flaky test’ler güveni hiç test olmamaktan daha hızlı aşındırıyor. İşte onlara sebep olan şeyler ve nasıl düzeltilir:
Kaçınılacak Anti-pattern’ler#
// BAD: Static wait'ler flakiness'a sebep olur
await page.click('#submit');
await page.waitForTimeout(3000); // Çok kısa veya çok uzun olabilir
await page.click('#next-step');
// Auto-waiting timing'i hallediyor
await page.getByTestId('submit-button').click();
await expect(page.getByTestId('next-step-button')).toBeVisible();
// BAD: Unstable selector'ler UI değişiklikleriyle bozulur
await page.click('div.container > ul > li:nth-child(3) > button');
// Stable selector'ler refactoring'den kurtulur
await page.getByTestId('user-list-item-delete-button').click();
Retry Ayarları#
Retry’lar diagnostic tool’lar, çözüm değil. CI’da aralıklı infrastructure sorunlarını halletmek için kullan:
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
retries: process.env.CI ? 2 : 0, // Sadece CI'da retry
use: {
actionTimeout: 10000,
navigationTimeout: 30000,
trace: 'retain-on-failure', // Debug için kritik
screenshot: 'only-on-failure',
video: 'retain-on-failure'
}
});
CI/CD Entegrasyonu ve Sharding#
Parallel execution uzun bir suite’i kısa bir feedback loop’a çeviriyor; GitHub Actions Playwright’ın shard flag’ini doğrudan destekliyor:
# .github/workflows/e2e-tests.yml
name: E2E Tests
on: [push, pull_request]
jobs:
playwright-tests:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shardIndex: [1, 2, 3, 4, 5, 6, 7, 8]
shardTotal: [8]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
env:
PLAYWRIGHT_BLOB_OUTPUT_DIR: blob-report
- uses: actions/upload-artifact@v4
if: always()
with:
name: blob-report-${{ matrix.shardIndex }}
path: blob-report
retention-days: 1
merge-reports:
needs: playwright-tests
if: always()
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/download-artifact@v4
with:
pattern: blob-report-*
path: all-blob-reports
merge-multiple: true
- run: npx playwright merge-reports --reporter html ./all-blob-reports
- uses: actions/upload-artifact@v4
with:
name: html-report
path: playwright-report
retention-days: 14
Performans etkisi: aritmetiği basit. 35 dakikalık bir suite 8 shard’a bölündüğünde duvar saatinde yaklaşık 5 dakikada bitiyor, toplam runner-dakikası ise yaklaşık %14 artıyor (her biri 5 dakikalık 8 shard, 35 dakikalık sıralı koşuya karşı). Ekipler runner-dakikasından çok duvar saatini hissediyor; bu yüzden takas genelde kendini ödüyor.
Test Verisi Yönetimi#
Temiz test verisi alışkanlıkları testlerin birbirine karışmasını önlüyor ve güvenilirliği artırıyor.
Factory Pattern#
// test-data/factories.ts
import { Page } from '@playwright/test';
export class UserFactory {
static async create(page: Page, overrides?: Partial<User>) {
const userData = {
email: `test-${Date.now()}@example.com`,
name: 'Test Kullanıcı',
role: 'member',
...overrides
};
// Kayıt formunu tıklamak yerine API üzerinden oluştur
const response = await page.request.post('/api/users', {
data: userData
});
return response.json();
}
static async cleanup(page: Page, userId: string) {
await page.request.delete(`/api/users/${userId}`);
}
}
// Testlerde kullanımı
test('kullanıcı profilini güncelleyebilir', async ({ page }) => {
const user = await UserFactory.create(page);
await page.goto(`/profile/${user.id}`);
await page.getByTestId('profile-name').fill('Güncel İsim');
await page.getByTestId('profile-save').click();
await expect(page.getByTestId('profile-name')).toHaveValue('Güncel İsim');
await UserFactory.cleanup(page, user.id);
});
Playwright Fixture’ları#
Fixture’lar setup ve teardown’ı otomatik hallediyor:
// fixtures/index.ts
import { test as base } from '@playwright/test';
export const test = base.extend({
authenticatedUser: async ({ page }, use) => {
const user = await UserFactory.create(page, { role: 'user' });
await loginAs(page, user);
await use(user);
await UserFactory.cleanup(page, user.id);
},
adminUser: async ({ page }, use) => {
const admin = await UserFactory.create(page, { role: 'admin' });
await loginAs(page, admin);
await use(admin);
await UserFactory.cleanup(page, admin.id);
}
});
// Temiz test kodu
test('kullanıcı sepete ürün ekleyebilir', async ({ authenticatedUser, page }) => {
await page.goto('/products');
await page.getByTestId('product-add-to-cart').first().click();
await expect(page.getByTestId('cart-count')).toHaveText('1');
});
Görsel Regresyon Testleri#
Görsel regresyonlar functional test’lere yakalanmıyor. Otomatik screenshot karşılaştırması onları yakalıyor.
Playwright ile Yerleşik Görsel Test#
test('dashboard layout tutarlı kalıyor', async ({ page }) => {
await page.goto('/dashboard');
// Dinamik içeriğin yüklenmesini bekle
await page.waitForLoadState('networkidle');
// Dinamik elementleri maskele
await expect(page).toHaveScreenshot('dashboard.png', {
mask: [
page.getByTestId('user-greeting'), // Timestamp içeriyor
page.getByTestId('notification-badge') // Dinamik sayı
],
maxDiffPixels: 100
});
});
Gotcha: Screenshot’lar OS-dependent. macOS’ta çekilen screenshot Linux’la match etmez. Tutarlılık için visual test’leri Docker container’larında çalıştır:
# Dockerfile.test
FROM mcr.microsoft.com/playwright:v1.47.0-jammy
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
CMD ["npx", "playwright", "test"]
SaaS Alternatifleri#
Docker kompleksitesi olmadan cross-platform tutarlılığa ihtiyaç duyan ekipler için:
- Percy: AI destekli diff tespiti ve cross-browser render, ekip büyüklüğüne göre fiyatlanıyor
- Chromatic: Storybook entegrasyonu ve görsel onay akışı, snapshot hacmine göre fiyatlanıyor
- Lost Pixel (open-source): Percy’ye self-hosted alternatif
Trade-off: SaaS tool’lar paraya mal oluyor ama infrastructure management’ı ortadan kaldırıyor. Built-in çözümler ücretsiz ama containerization disiplini gerektiriyor.
Mobil Testler#
Web trafiğinin yarısından fazlası mobil cihazlardan geliyor. Sadece masaüstünü test etmek kritik sorunları kaçırıyor.
Cihaz Emülasyonu#
import { test, devices } from '@playwright/test';
// Önceden yapılandırılmış cihaz kullan
test.use(devices['iPhone 14 Pro']);
test('mobil navigasyon çalışıyor', async ({ page }) => {
await page.goto('/');
// Touch event'ler otomatik etkin
await page.getByTestId('mobile-menu-button').tap();
await expect(page.getByTestId('mobile-nav')).toBeVisible();
});
// Birden fazla cihaz test et
const mobileDevices = ['iPhone 14 Pro', 'Pixel 5', 'Galaxy S24'];
for (const deviceName of mobileDevices) {
test.describe(deviceName, () => {
test.use(devices[deviceName]);
test('checkout akışı tamamlanıyor', async ({ page }) => {
await page.goto('/checkout');
// Test viewport'a adapte oluyor
});
});
}
Konum Testleri#
test.use({
geolocation: { longitude: 29.0104, latitude: 41.0082 },
permissions: ['geolocation']
});
test('konuma göre yakın mağazaları gösteriyor', async ({ page }) => {
await page.goto('/stores');
await expect(page.getByTestId('store-location'))
.toContainText('İstanbul');
// Test ortasında konum değiştir
await page.context().setGeolocation({
longitude: 32.8597,
latitude: 39.9334
});
await page.reload();
await expect(page.getByTestId('store-location'))
.toContainText('Ankara');
});
Erişilebilirlik Testleri#
Otomatik kontroller mekanik WCAG ihlallerini yakalıyor: eksik label, yetersiz kontrast, yanlış kullanılan ARIA. Yorum gerektiren kısımlar için hâlâ ekrana bakan bir insan gerekiyor. Otomatik kısmı her test run’ına bağla ki manuel inceleme daha kısa bir listeyle başlasın.
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test('anasayfa WCAG 2.1 AA standartlarını karşılıyor', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21aa'])
.exclude('#third-party-widget') // Kontrol etmediğin external widget'lar
.analyze();
expect(results.violations).toEqual([]);
});
test('klavye navigasyonu uygulama boyunca çalışıyor', async ({ page }) => {
await page.goto('/');
// İnteraktif elementler arasında tab ile gezin
await page.keyboard.press('Tab');
await expect(page.getByTestId('search-input')).toBeFocused();
await page.keyboard.press('Tab');
await expect(page.getByTestId('nav-link-about')).toBeFocused();
await page.keyboard.press('Tab');
await expect(page.getByTestId('nav-link-products')).toBeFocused();
});
Kademeli benimseme için, başlangıçta testleri fail etmeden violation’ları logla:
const results = await new AxeBuilder({ page }).analyze();
if (results.violations.length > 0) {
console.warn(`[WARN] ${results.violations.length} accessibility violation bulundu:`);
results.violations.forEach(violation => {
console.warn(` ${violation.id}: ${violation.description}`);
console.warn(` Etki: ${violation.impact}`);
console.warn(` Etkilenen elementler: ${violation.nodes.length}`);
});
}
Component vs E2E Testing#
Her şey E2E testing gerektirmiyor. Test pyramid hala geçerli.
Pratik Dağılım#
- %70 Unit/Component test’ler: Business logic, edge case’ler, hesaplamalar
- %20 Integration test’ler: API + component interaction, multi-step workflow’lar
- %10 E2E test’ler: Kritik user journey’ler (login, satın alma, kayıt)
Doğru seviyede test etme örneği:
// BAD: Edge case'leri E2E seviyesinde test etme
test('kupon kodu validasyonu: süresi geçmiş kuponlar', async ({ page }) => {
await page.goto('/');
await page.getByTestId('product-add').click();
await page.getByTestId('checkout').click();
await page.getByTestId('coupon-input').fill('EXPIRED2020');
await page.getByTestId('coupon-apply').click();
await expect(page.getByTestId('error')).toContainText('süresi geçmiş');
});
// Component seviyesinde test et
// tests/components/CouponValidator.test.ts
test('süresi geçmiş kupon kodlarını reddeder', () => {
const validator = new CouponValidator();
expect(validator.validate('EXPIRED2020')).toEqual({
valid: false,
error: 'Kuponun süresi geçmiş'
});
});
// E2E testler happy path'lere odaklanıyor
test('kullanıcı geçerli kuponla satın almayı tamamlar', async ({ page }) => {
await page.goto('/');
await page.getByTestId('product-add').click();
await page.getByTestId('checkout').click();
await page.getByTestId('coupon-input').fill('SAVE20');
await page.getByTestId('coupon-apply').click();
await expect(page.getByTestId('discount')).toContainText('20 TL');
await page.getByTestId('complete-order').click();
await expect(page.getByTestId('confirmation')).toBeVisible();
});
Yaygın Tuzaklar ve Çözümler#
Tuzak 1: E2E Test’lere Aşırı Güven#
Belirti: Test suite 30+ dakika sürüyor, çoğunlukla unit-level bug’ları yakalıyor.
Çözüm: Edge case’leri component test’lere taşı. E2E’yi kritik user path’lar için ayır.
Tuzak 2: Flaky Test’leri Görmezden Gelme#
Belirti: “Tekrar çalıştır” kültürü güveni yok ediyor.
Çözüm: Flakiness metriklerini takip et. Flaky test’leri, ekip kırmızı koşuları görmezden gelmeyi öğrenmeden önce karantinaya al veya düzelt.
Tuzak 3: Test Isolation Eksikliği#
Belirti: Testler tek başına pass oluyor ama suite’te fail, sıraya bağımlı hatalar.
Çözüm: Her test izole çalıştırılabilir olmalı. Setup için factory’leri kullan, teardown’da temizle.
Tuzak 4: Trace Viewer Kullanmamak#
Belirti: CI hatalarını local’de debug için saatler harcama.
Çözüm: Config’de trace: 'retain-on-failure' etkinleştir. CI artifact’larından trace dosyalarını indir ve npx playwright show-trace trace.zip ile aç. Viewer DOM snapshot’ları, network call’ları, console log’ları ve exact timing gösteriyor; saatler kazandırıyor.
Tuzak 5: Her Şeyi Mocklamak#
Belirti: Tüm API call’lar mock’lanmış, testler pass ama production bozuk.
Çözüm: External third-party’leri ve error senaryolarını mockla. E2E testlerinde kendi API’ni mocklama; bu integration testing amacını bozuyor.
Varsayılanı Ne Zaman Değiştirmeli#
Playwright varsayılanı; suite CI’da koşuyorsa, Chromium’un ötesinde kapsama ihtiyaç duyuyorsa ve tek makinenin makul sürede bitiremeyeceği kadar büyüyorsa geçerli. Tek tarayıcılı bir SPA üzerinde gün boyu interaktif debug yapan ve Cypress Cloud bütçesi zaten ayrılmış bir ekip için Cypress daha doğru tercih. Onlar için interaktif runner ücretsiz shard’lardan daha değerli.
Hangisini seçersen seç page object’ler, data-testid selector’ler ve API üzerinden oluşturulan test verisi taşınıyor; sonradan framework değiştirmek göründüğü kadar pahalıya patlamıyor. 5-10 kritik path testiyle başla ve düzeltilene kadar flaky olanları suite dışında tut. Kapsamı, bu testler yayına gidecek bir regresyonu yakaladıktan sonra genişlet.
Kaynaklar#
- Playwright Dokümantasyonu (yeni sekmede açılır) - Resmi Playwright E2E test framework belgeleri
- Cypress Dokümantasyonu (yeni sekmede açılır) - Resmi Cypress E2E test rehberi
- Pratik Test Piramidi - Martin Fowler (yeni sekmede açılır) - Birim, entegrasyon ve E2E testlerini dengelemek için yetkili rehber
- Test Piramidi - Martin Fowler bliki (yeni sekmede açılır) - Test piramidi kavramının özlü tanımı
- Playwright: Test Yazma (yeni sekmede açılır) - Otomatik bekleme, doğrulama ve test yapısı kalıpları
İlgili yazılar
Organizasyon düzeyinde paylaşımlı bir GitHub Actions platformu kurma rehberi: mimari kararlar, güvenlik yönetişimi, benimseme ve en maliyetli 7 hata.
github-actions · ci-cd · devops +5
AI destekli kod incelemesinin neyi yakaladığı, insanların hangi alanlarda üstün kaldığı ve etkili insan-AI işbirliğinin kod incelemesinde nasıl kurulacağı.
code-review · ci-cd · security +7
Acele etmek hızlı hissettirir ama yeniden iş, hata ve yangın söndürme yaratır. Refactor, test ve CI bakımı için durmak neden hız kaybı değil, hıza yatırımdır.
technical-debt · testing · ci-cd +2
Bruno .bru dosyalarını repo'ya commit etmek, API sözleşmesini kodla aynı PR ve geçmişte tutar. Tek gerçek bedel, bilinçli bir secret sınırıdır.
testing · ci-cd · developer-experience +1
Distributed sistemlerde feature flag için production rehberi: LaunchDarkly, Unleash ve AWS AppConfig karşılaştırması, rollout ve A/B testing örnekleri.
feature-flags · devops · ci-cd +5