Skip to content
~/sph.sh

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:

typescript
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:

typescript
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

typescript
// page-objects/LoginPage.tsimport { 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ı:

typescript
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}.

html
<!-- İ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:

typescript
// Daha iyi: Accessible role kullanıyorawait page.getByRole('button', { name: 'Sepete Ekle' }).click();
// İyi: Explicit test IDawait page.getByTestId('add-to-cart-button').click();
// Kırılgan: Implementation-dependentawait 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 Native Mocking

Client-side app'ler için page.route() çoğu durumu hallediyor:

typescript
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:

typescript
// mocks/handlers.tsimport { 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:

typescript
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

typescript
// ❌ Static wait'ler flakiness'a sebep olurawait page.click('#submit');await page.waitForTimeout(3000); // Çok kısa veya çok uzun olabilirawait page.click('#next-step');
// ✅ Auto-waiting timing'i hallediyorùawait page.getByTestId('submit-button').click();await expect(page.getByTestId('next-step-button')).toBeVisible();
typescript
// ❌ Unstable selector'ler UI değişiklikleriyle bozulurawait page.click('div.container > ul > li:nth-child(3) > button');
// ✅ Stable selector'ler refactoring'den kurtulurawait 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:

typescript
// playwright.config.tsimport { 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:

yaml
# .github/workflows/e2e-tests.ymlname: E2E Testson: [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

typescript
// test-data/factories.tsimport { 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:

typescript
// fixtures/index.tsimport { 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 kodutest('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

typescript
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
# Dockerfile.testFROM mcr.microsoft.com/playwright:v1.47.0-jammy
WORKDIR /appCOPY package*.json ./RUN npm ciCOPY . .
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

typescript
import { test, devices } from '@playwright/test';
// Önceden yapılandırılmış cihaz kullantest.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 etconst 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

typescript
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.

typescript
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:

typescript
const results = await new AxeBuilder({ page }).analyze();
if (results.violations.length > 0) {  console.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:

typescript
// ❌ Edge case'leri E2E seviyesinde test etmetest('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.tstest('gecmis kupon kodlarini reddeder', () => {  const validator = new CouponValidator();  expect(validator.validate('EXPIRED2020')).toEqual({    valid: false,    error: 'Kupon gecmis'  });});
// ✅ E2E testler happy path'lere odaklanirtest('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

  1. 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.

  2. 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.

  3. Flakiness bir bug: Auto-waiting çoğu timing sorununu ortadan kaldırıyor. Flakiness metriklerini takip et ve agresif düzelt.

  4. Test pyramid'i dengele: %70 component, %20 integration, %10 E2E. Edge case'leri E2E seviyesinde test etme.

  5. Mobile testing opsiyonel değil: Device emulation mobil sorunların %95'ini kapsıyor. Viewport'ları, touch interaction'ları ve mobile performance'ı test et.

  6. Accessibility'yi otomatikleştir: axe-core entegrasyonu WCAG ihlallerinin %30-40'ını otomatik yakalıyor. Tam kapsam için manual testing hala gerekli.

  7. API-first test data: API üzerinden data oluşturmak UI navigation'dan 10-50x daha hızlı. Factory'leri ve fixture'ları kullan.

  8. Visual regression disiplin gerektirir: Docker container'ları cross-platform tutarlılığı sağlıyor. Dinamik içeriği maskele. Makul diff threshold'lar belirle.

  9. 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.

  10. 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