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.
