2025-12-20
Modern Web Uygulamaları için E2E Testing Stratejileri - Pratik Bir Mühendislik Rehberi
Playwright ve Cypress ile güvenilir, sürdürülebilir E2E test suite'leri nasıl oluşturulur öğrenin. Framework seçimi, flaky test önleme, CI/CD entegrasyonu ve gerçek dünya optimizasyon stratejilerini kapsıyor.
Özet
End-to-end testing, Playwright ve Cypress gibi modern framework’lerle önemli ölçüde gelişti. Bu rehber, gerçek bug’ları yakalarken flakiness’ı minimize eden güvenilir E2E test suite’leri oluşturmak için pratik stratejileri inceliyor. Framework seçimi, mimari pattern’ler, API mocking, visual regression, accessibility testing ve CI/CD optimizasyonunu kapsıyoruz. Bu araçlarla çalışırken öğrendiğim şey, başarının tool seçiminden çok mimari kararlardan geldiği; doğru test isolation, stable selector’ler ve dengeli test pyramid’leri hangi framework’ü seçtiğinden daha önemli.
Framework Seçimi: Playwright vs Cypress
Mimari Farklar
Playwright ve Cypress arasındaki seçim, birinin daha iyi olmasıyla ilgili değil; yetenekleri gereksinimlerle eşleştirmekle ilgili. Farklı senaryolarda neyin işe yaradığı:
Çalışan Örnekler
İşte auto-waiting’i gösteren basit bir Playwright testi:
import { test, expect } from '@playwright/test';
test('kullanici satin alma flow\'unu 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 Yilmaz');
await page.getByTestId('shipping-address').fill('Ataturk 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('Satin Alma Flow', () => {
it('kullanicinin satin alma tamamlamasina 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 Yilmaz');
cy.get('[data-testid="shipping-address"]').type('Ataturk 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 Implementation
// 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('gecerli credential\'lar login\'e izin verir', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('[email protected]', 'password123');
await loginPage.expectLoginSuccess();
});
test('gecersiz credential\'lar hata gosterir', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('[email protected]', 'wrongpassword');
await loginPage.expectLoginError('Gecersiz kimlik bilgileri');
});
Selector Stability
Test edeceğin elementler için data-testid attribute’ları kullan. Benim faydalı bulduğum 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ı.
graph LR
A[API Mocking Strategy] --> B{Rendering Type}
B -->|Client-side only| C[Playwright page.route]
B -->|Server-side SSR/SSG| D[MSW with Next.js proxy]
B -->|Both CSR + SSR| E[MSW + Playwright integration]
C --> F[Simple route mocking]
F --> F1[Hızlı kurulum]
F --> F2[Service worker overhead yok]
D --> G[MSW browser mode]
G --> G1[Vitest/Storybook\'ta tekrar kullanılabilir]
G --> G2[Full Request/Response API]
E --> H[Hybrid yaklasim]
H --> H1[@msw/playwright package]
H --> H2[window.msw pattern]
Playwright Native Mocking
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('Urunler yuklenemedi');
});
MSW ile Comprehensive Mocking
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: 'Urun 1', price: 299.99 },
{ id: 2, name: 'Urun 2', price: 399.99 }
]);
}),
http.post('/api/orders', async ({ request }) => {
const body = await request.json();
return HttpResponse.json(
{ orderId: '12345', status: 'confirmed' },
{ status: 201 }
);
})
];
Playwright ile entegrasyon:
import { setupWorker } from 'msw/browser';
import { handlers } from './mocks/handlers';
test.beforeEach(async ({ page }) => {
// MSW worker'ı browser context'ine yükle
await page.addInitScript(() => {
const { setupWorker } = require('msw/browser');
const { handlers } = require('./mocks/handlers');
const worker = setupWorker(...handlers);
worker.start();
});
});
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 Configuration
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 35 dakikalık test suite’leri 5 dakikalık feedback loop’lara dönüştürüyor. GitHub Actions bunu basitleştiriyor:
# .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
Performance impact: Yakın zamandaki bir projede, bu test execution’ı 35 dakikadan 5 dakikaya düşürdü; 7 kat iyileşme. Maliyet yaklaşık %14 arttı (8 concurrent runner vs. 1 sequential), bu da daha hızlı feedback için kolayca justify edildi.
Test Data Management
Temiz test data practice’leri testler arası müdahaleyi önlüyor ve reliability’yi 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 Kullanici',
role: 'member',
...overrides
};
// API üzerinden oluştur (UI'dan 10-50x daha hızlı)
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('kullanici profilini guncelleyebilir', async ({ page }) => {
const user = await UserFactory.create(page);
await page.goto(`/profile/${user.id}`);
await page.getByTestId('profile-name').fill('Guncel Isim');
await page.getByTestId('profile-save').click();
await expect(page.getByTestId('profile-name')).toHaveValue('Guncel Isim');
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('kullanici sepete urun 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');
});
Visual Regression Testing
Visual regression’lar functional test’lerden geçiyor. Otomatik screenshot karşılaştırması onları yakalıyor.
Playwright Built-in Visual Testing
test('dashboard layout tutarli kaliyor', 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-powered diff detection, cross-browser (fiyatlandırma ekip büyüklüğüne göre değişiyor; güncel fiyatları kontrol et)
- Chromatic: Storybook entegrasyonu, visual approval workflow (fiyatlandırma snapshot sayısına göre değişiyor; güncel fiyatları kontrol et)
- 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.
Mobile Testing
Web trafiğinin yarısından fazlası mobil cihazlardan geliyor. Sadece desktop test etmek kritik sorunları kaçırıyor.
Device Emulation
import { test, devices } from '@playwright/test';
// Önceden yapılandırılmış cihaz kullan
test.use(devices['iPhone 14 Pro']);
test('mobil navigasyon calisiyor', 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 flow tamamlaniyor', async ({ page }) => {
await page.goto('/checkout');
// Test viewport'a adapte oluyor
});
});
}
Geolocation Testing
test.use({
geolocation: { longitude: 29.0104, latitude: 41.0082 },
permissions: ['geolocation']
});
test('konuma gore yakin magazalari gosteriyor', async ({ page }) => {
await page.goto('/stores');
await expect(page.getByTestId('store-location'))
.toContainText('Istanbul');
// 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');
});
Accessibility Testing
Otomatik accessibility testing WCAG ihlallerinin %30-40’ını yakalıyor. Her test run’a entegre et.
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test('anasayfa WCAG 2.1 AA standartlarini karsilı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 calisiyor', 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: gecmis 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('gecmis');
});
// Component seviyesinde test et
// tests/components/CouponValidator.test.ts
test('gecmis kupon kodlarini reddeder', () => {
const validator = new CouponValidator();
expect(validator.validate('EXPIRED2020')).toEqual({
valid: false,
error: 'Kupon gecmis'
});
});
// E2E testler happy path'lere odaklanir
test('kullanici gecerli kuponla satin 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 hemen karantinaya al veya düzelt. Flaky bir test suite hiç test olmamasından kötü.
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.
Önemli Çıkarımlar
-
Framework seçimi mimariden daha az önemli: Page Object Model, stable selector’ler ve doğru test isolation hem Playwright hem Cypress’te çalışıyor.
-
Hız için parallelize et: 8-way sharding execution’ı 35 dakikadan 5 dakikaya düşürdü; daha hızlı feedback için %14 maliyet artışına değer.
-
Flakiness bir bug: Auto-waiting çoğu timing sorununu ortadan kaldırıyor. Flakiness metriklerini takip et ve agresif düzelt.
-
Test pyramid’i dengele: %70 component, %20 integration, %10 E2E. Edge case’leri E2E seviyesinde test etme.
-
Mobile testing opsiyonel değil: Device emulation mobil sorunların %95’ini kapsıyor. Viewport’ları, touch interaction’ları ve mobile performance’ı test et.
-
Accessibility’yi otomatikleştir: axe-core entegrasyonu WCAG ihlallerinin %30-40’ını otomatik yakalıyor. Tam kapsam için manual testing hala gerekli.
-
API-first test data: API üzerinden data oluşturmak UI navigation’dan 10-50x daha hızlı. Factory’leri ve fixture’ları kullan.
-
Visual regression disiplin gerektirir: Docker container’ları cross-platform tutarlılığı sağlıyor. Dinamik içeriği maskele. Makul diff threshold’lar belirle.
-
Debugging tool’larına yatırım yap: Trace viewer, screenshot’lar ve fail olan testler için video’lar kendilerini hızlıca geri ödüyor.
-
Küçük başla, iterate et: 5-10 kritik path test ile başla. Coverage’ı genişletmeden önce değeri kanıtla.
E2E testing kapsamlı bir testing stratejisinde bir katman olarak ele alındığında en iyi sonucu veriyor. Kritik path’lerle başla, doğru mimariyle flakiness’ı önle ve parallelization ile scale et.
İlgili yazılar
Organizasyon düzeyinde paylaşımlı bir GitHub Actions platformu kurmak için pratik bir rehber: mimari kararlar, güvenlik yönetişimi, benimseme stratejisi ve bu süreçte yaptığımız en büyük 7 hata.
TypeScript microservislerde consumer-driven contract testing'i Pact ile uygulamaya yönelik pratik bir kılavuz. Breaking API değişikliklerini deployment öncesi yakalayın ve integration test yükünü azaltın.
AWS Lambda, API Gateway, DynamoDB ve Step Functions için hızlı geri bildirim ve production güvenilirliği sağlayan kapsamlı bir test stratejisi oluşturmayı öğrenin.
Gerçek kurumsal deneyimlere dayalı AI destekli kod incelemesi uygulama rehberi. AI'ın insanların kaçırdığını ne yakaladığını, insanların hala üstün olduğu alanları ve kod inceleme süreçlerinde etkili insan-AI işbirliği kurmayı öğrenin.
Takım büyüklüğü, ürün tipi ve gerçek başarısızlıklara dayanan Git branching stratejileri hakkında acımasızca dürüst bir rehber.