Cypress ile End-to-End Web Uygulama Testi: Kapsamlı Rehber

Production ortamında bir frontend deploy ettikten sonra kullanıcılardan “ödeme sayfası çalışmıyor” mesajı almak, her sysadmin ve DevOps mühendisinin kabusu. Oysa o sabah tüm unit testler yeşildi, CI pipeline tertemiz geçmişti. Sorun şuydu: hiç kimse gerçek bir tarayıcıda, gerçek bir kullanıcı gibi tüm akışı baştan sona test etmemişti. İşte Cypress tam bu noktada devreye giriyor.

Cypress Nedir ve Neden Önemli?

Cypress, modern web uygulamaları için geliştirilmiş bir end-to-end (E2E) test framework’ü. Selenium gibi WebDriver tabanlı değil; doğrudan tarayıcı içinde çalışıyor. Bu mimarisi sayesinde testler hem daha hızlı hem de daha güvenilir oluyor. Flaky test denen, bazen geçip bazen geçmeyen testler Cypress ile ciddi ölçüde azalıyor.

Bir sysadmin veya DevOps mühendisi olarak şunu düşünebilirsiniz: “Bu benim işim değil, geliştirici yazsın bu testleri.” Evet, geliştirici yazabilir. Ama CI/CD pipeline’ınıza bu testleri entegre edecek, Docker container’ında headless çalıştıracak, test raporlarını artifact olarak saklayacak ve geceyarısı patlayan bir testi debug edecek olan genellikle sizsiniz. Dolayısıyla Cypress’i iyi tanımak şart.

Kurulum ve İlk Yapılandırma

Node.js 16+ gerektiriyor Cypress. Önce projeyi hazırlayalım:

mkdir cypress-demo && cd cypress-demo
npm init -y
npm install cypress --save-dev

İlk açılışta Cypress size örnek testler ve yapılandırma dosyaları oluşturuyor:

npx cypress open

Bu komut GUI’yi açar. CI ortamında GUI istemiyorsunuz, doğrudan headless modda çalıştırın:

npx cypress run

Proje kökünüzde cypress.config.js dosyası oluşturulur. Temel bir yapılandırma şöyle görünür:

const { defineConfig } = require('cypress')

module.exports = defineConfig({
  e2e: {
    baseUrl: 'http://localhost:3000',
    viewportWidth: 1280,
    viewportHeight: 720,
    video: true,
    screenshotOnRunFailure: true,
    defaultCommandTimeout: 8000,
    retries: {
      runMode: 2,
      openMode: 0
    },
    setupNodeEvents(on, config) {
      // Node event listener'ları buraya
    }
  }
})

retries ayarına dikkat edin. runMode: 2 diyorsunuz, yani CI’da bir test başarısız olursa 2 kez daha deniyor. Bu flaky testleri tolere etmek için iyi bir başlangıç noktası ama uzun vadede her flaky testi düzeltmeniz gerekiyor. Retry sayısını artırarak sorunu örtbas etmek teknik borç biriktiriyor.

İlk Gerçek Test: Login Akışı

cypress/e2e/ klasörü altına testlerinizi yazıyorsunuz. Gerçek dünya senaryosundan gidelim: bir e-ticaret uygulamasının login akışını test edelim.

// cypress/e2e/auth/login.cy.js

describe('Login Akışı', () => {
  beforeEach(() => {
    cy.visit('/login')
  })

  it('Geçerli kimlik bilgileriyle giriş yapılabilmeli', () => {
    cy.get('[data-testid="email-input"]').type('[email protected]')
    cy.get('[data-testid="password-input"]').type('GucluSifre123!')
    cy.get('[data-testid="login-button"]').click()

    // Dashboard'a yönlendirme kontrolü
    cy.url().should('include', '/dashboard')
    cy.get('[data-testid="user-menu"]').should('be.visible')
    cy.get('[data-testid="welcome-message"]')
      .should('contain', 'Hoş geldiniz')
  })

  it('Yanlış şifreyle hata mesajı gösterilmeli', () => {
    cy.get('[data-testid="email-input"]').type('[email protected]')
    cy.get('[data-testid="password-input"]').type('YanlisŞifre')
    cy.get('[data-testid="login-button"]').click()

    cy.get('[data-testid="error-alert"]')
      .should('be.visible')
      .and('contain', 'Geçersiz kimlik bilgileri')

    // URL login sayfasında kalmalı
    cy.url().should('include', '/login')
  })

  it('Boş form gönderilince validasyon hataları gösterilmeli', () => {
    cy.get('[data-testid="login-button"]').click()

    cy.get('[data-testid="email-error"]').should('be.visible')
    cy.get('[data-testid="password-error"]').should('be.visible')
  })
})

Burada data-testid attribute’larını kullandım. Bu önemli bir karar. cy.get('.btn-primary') gibi CSS sınıfına bağlı selector yazmak, bir geliştirici tasarımı değiştirdiğinde testlerinizi kırar. data-testid kullanmak testleri implementasyon detaylarından izole eder. Geliştiricilerle bu konuşmayı erkenden yapın.

Custom Commands ile Tekrarlı Kodu Azaltmak

Her test dosyasında login işlemini tekrarlamamak için custom command yazıyoruz. cypress/support/commands.js dosyasına:

// cypress/support/commands.js

Cypress.Commands.add('login', (email, password) => {
  cy.request({
    method: 'POST',
    url: '/api/auth/login',
    body: { email, password }
  }).then((response) => {
    window.localStorage.setItem('authToken', response.body.token)
    window.localStorage.setItem('user', JSON.stringify(response.body.user))
  })
})

Cypress.Commands.add('loginViaUI', (email = '[email protected]', password = 'GucluSifre123!') => {
  cy.visit('/login')
  cy.get('[data-testid="email-input"]').type(email)
  cy.get('[data-testid="password-input"]').type(password)
  cy.get('[data-testid="login-button"]').click()
  cy.url().should('include', '/dashboard')
})

Dikkat edin: cy.login() komutu UI üzerinden değil, direkt API isteğiyle token alıyor ve localStorage’a yazıyor. Bu yaklaşım testleri ciddi ölçüde hızlandırıyor. Login sayfasını sadece login sayfasını test ederken UI ile test etmek yeterli. Diğer testlerde cy.login() ile direkt authenticated state’e geçin.

Fixture Kullanımı ve Test Verisi Yönetimi

Test verileri kodun içine gömülmemelidir. cypress/fixtures/ klasörüne JSON dosyaları koyarsınız:

// cypress/fixtures/users.json
{
  "admin": {
    "email": "[email protected]",
    "password": "Admin123!",
    "role": "ADMIN"
  },
  "regularUser": {
    "email": "[email protected]",
    "password": "User123!",
    "role": "USER"
  },
  "readOnlyUser": {
    "email": "[email protected]",
    "password": "Viewer123!",
    "role": "VIEWER"
  }
}

Bunu test dosyasında kullanmak için:

// cypress/e2e/dashboard/permissions.cy.js

describe('Dashboard Yetki Kontrolleri', () => {
  let users

  before(() => {
    cy.fixture('users').then((data) => {
      users = data
    })
  })

  it('Admin kullanıcı ayarlar menüsünü görebilmeli', () => {
    cy.login(users.admin.email, users.admin.password)
    cy.visit('/dashboard')
    cy.get('[data-testid="settings-menu"]').should('be.visible')
    cy.get('[data-testid="user-management"]').should('be.visible')
  })

  it('Regular kullanıcı ayarlar menüsünü göremememeli', () => {
    cy.login(users.regularUser.email, users.regularUser.password)
    cy.visit('/dashboard')
    cy.get('[data-testid="settings-menu"]').should('not.exist')
  })
})

API Intercept: Ağ İsteklerini Kontrol Altına Almak

Cypress’in en güçlü özelliklerinden biri cy.intercept(). Ağ isteklerini yakalayıp manipüle edebiliyorsunuz. Bu sayede backend hazır olmasa bile frontend testlerini yazabilirsiniz.

// cypress/e2e/products/product-list.cy.js

describe('Ürün Listesi', () => {
  it('Ürünler başarıyla yüklenince listelenmeli', () => {
    cy.intercept('GET', '/api/products*', {
      statusCode: 200,
      fixture: 'products.json'
    }).as('getProducts')

    cy.login('[email protected]', 'GucluSifre123!')
    cy.visit('/products')
    cy.wait('@getProducts')

    cy.get('[data-testid="product-card"]').should('have.length', 10)
  })

  it('API hatası durumunda hata mesajı gösterilmeli', () => {
    cy.intercept('GET', '/api/products*', {
      statusCode: 500,
      body: { message: 'Sunucu hatası' }
    }).as('getProductsError')

    cy.login('[email protected]', 'GucluSifre123!')
    cy.visit('/products')
    cy.wait('@getProductsError')

    cy.get('[data-testid="error-state"]')
      .should('be.visible')
      .and('contain', 'Bir hata oluştu')
    cy.get('[data-testid="retry-button"]').should('be.visible')
  })

  it('Yükleme sırasında skeleton gösterilmeli', () => {
    cy.intercept('GET', '/api/products*', (req) => {
      req.reply((res) => {
        res.setDelay(2000)
      })
    }).as('slowProducts')

    cy.login('[email protected]', 'GucluSifre123!')
    cy.visit('/products')

    cy.get('[data-testid="skeleton-loader"]').should('be.visible')
    cy.wait('@slowProducts')
    cy.get('[data-testid="skeleton-loader"]').should('not.exist')
  })
})

Bu son test özellikle değerli. Skeleton loader’ın gerçekten gösterilip gösterilmediğini test etmek için normalde network’ü yavaşlatmanız gerekir. Cypress ile bu çok kolay.

CI/CD Entegrasyonu: GitHub Actions Örneği

Testleri yerel makinede çalıştırmak güzel ama asıl değer CI’da her PR’da otomatik çalışmasında. İşte gerçekte kullandığım bir GitHub Actions workflow:

# .github/workflows/e2e-tests.yml
name: E2E Tests

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  cypress-run:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        containers: [1, 2, 3]

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Node.js Kurulum
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Bağımlılıkları Yükle
        run: npm ci

      - name: Uygulamayı Başlat
        run: npm run start:ci &
        env:
          NODE_ENV: test
          DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }}

      - name: Uygulama Hazır Olana Kadar Bekle
        run: npx wait-on http://localhost:3000 --timeout 60000

      - name: Cypress Testlerini Çalıştır
        uses: cypress-io/github-action@v6
        with:
          record: true
          parallel: true
          group: 'E2E Tests - ${{ matrix.containers }}'
        env:
          CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Test Artifact'larını Kaydet
        uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: cypress-artifacts-${{ matrix.containers }}
          path: |
            cypress/screenshots
            cypress/videos
          retention-days: 7

fail-fast: false önemli. Bir container başarısız olsa bile diğerleri çalışmaya devam ediyor, daha kapsamlı hata raporu alıyorsunuz. retention-days: 7 ile artifact’ları 7 gün saklıyorsunuz, disk maliyetini de kontrol altında tutuyorsunuz.

Docker ile Cypress Çalıştırmak

Bazı durumlarda kendi CI runner’ınız var ve Cypress’i Docker içinde çalıştırmak istiyorsunuz. Cypress’in resmi Docker image’ları mevcut:

# Headless Chrome ile tek seferlik çalıştırma
docker run --rm 
  -v $(pwd):/e2e 
  -w /e2e 
  --network host 
  -e CYPRESS_BASE_URL=http://localhost:3000 
  cypress/included:13.6.0

# Docker Compose ile uygulama + Cypress birlikte
# docker-compose.test.yml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      NODE_ENV: test
      DATABASE_URL: postgresql://postgres:password@db:5432/testdb
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: testdb
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5

  cypress:
    image: cypress/included:13.6.0
    depends_on:
      - app
    environment:
      CYPRESS_BASE_URL: http://app:3000
    volumes:
      - ./cypress:/e2e/cypress
      - ./cypress.config.js:/e2e/cypress.config.js
    working_dir: /e2e
    command: cypress run --browser chrome

Bu compose dosyasıyla docker-compose -f docker-compose.test.yml up --exit-code-from cypress komutunu çalıştırırsınız. Cypress container’ı kapandığında diğerleri de kapanır ve exit code Cypress’ten gelir.

Yaygın Sorunlar ve Çözümleri

Flaky Testler

En çok şikayet edilen konu bu. Genellikle şu sebeplerden oluşur:

  • Animasyon beklememek: cy.get() elementi buluyor ama animasyon henüz bitmemiş. { timeout: 10000 } geçmek yerine animasyonun bitmesini bekleyin.
  • Test izolasyonu eksikliği: Bir testin bıraktığı state diğerini etkiliyor. Her test kendi state’ini temizlemeli.
  • Race condition: API isteği bitmeden assertion yapılıyor. cy.wait('@alias') kullanın.

Slow Testler

Tüm akışları UI üzerinden yapmayın. Sadece test ettiğiniz şeyi UI ile yapın, geri kalanı API çağrısıyla hazırlayın. 10 saniyeden uzun süren test sayısını minimumda tutun.

Büyük Test Suite’i Organize Etmek

  • cypress/e2e/ altında feature bazlı klasörler açın
  • Kritik akışları (auth/, checkout/, payment/) ayrı tutun
  • Smoke test dosyaları oluşturun, sadece en kritik 5-10 testi içersin, her deployment sonrası bu çalışsın

Sonuç

Cypress’i production ortamınıza entegre etmek başlangıçta zaman alıyor ama yatırımın geri dönüşü çok net. Bizim deneyimimizde, E2E test suite’i ekledikten sonra production’a çıkan regression bug sayısı dramatik biçimde düştü. Özellikle ödeme akışı, kullanıcı kayıt ve kritik form validasyonları gibi yüksek değerli akışları kapsayan testler, her deployment öncesi gece uyku düzeninizi korumaya yardımcı oluyor.

Başlarken her şeyi test etmeye çalışmayın. Önce en kritik iki veya üç akışı seçin, onları sağlam yazın, CI’a entegre edin. Testlerin değerini görünce ekip kendiliğinden genişletmeye başlar.

Son bir not: Cypress testleri de kod. Code review’dan geçsin, aynı kalite standartları uygulansın. “Test kodudur, fazla önemli değil” diye düşünmek ilerleyen süreçte bakımı zor, güvenilmez bir test suite’iyle baş başa kalmanıza yol açar.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir