CI süresi neden ilk duvara çarpar
Suite 40 dakikaya çıktığında ekip test yazmayı bırakır. PR açan kişi sonucu beklemek yerine başka işe geçer, geri döndüğünde context’i kaybetmiştir, kırmızı gelen testi incelemek yerine yeniden çalıştırır. Bu noktada problem “testler yavaş” diye adlandırılır ve yanlış yere müdahale edilir: testler kısaltılır, assertion’lar silinir, kritik senaryolar nightly’ye sürülür.
Gerçek tanım şu: suite tek makinede sıralı çalışıyor. Toplam süre test sayısıyla lineer artıyor çünkü aynı anda bir tane şey yapılıyor. Çözümün iki ekseni var ve bunlar birbirinin yerine geçmez — makine içinde paralel süreç (worker) ve makineler arasında bölünmüş parça (shard). Hangisini artıracağına karar vermeden config dosyasına dokunursan ya runner’ı boğarsın ya faturayı şişirirsin.
Worker ve shard aynı şey değil
Worker, Playwright’ın tek makinede açtığı paralel işçi sürecidir. Her worker kendi browser context’ini kurar, kendi test dosyasını alır, aynı CPU ve belleği diğer worker’larla paylaşır. Shard ise suite’in CI job’larına bölünmüş dilimidir; her shard ayrı bir runner’da, ayrı bir makinede, kendi worker’larıyla çalışır.

workers=4 ile shard=4 tamamen farklı maliyet profili üretir. Dört worker tek runner ücreti öder ama dört Chromium örneğini aynı iki çekirdeğe sıkıştırır. Dört shard dört runner ücreti öder, kurulum maliyetini dört kez tekrarlar, karşılığında gerçek donanım kazandırır.
Karar kriteri üç şey: runner’ın çekirdek ve bellek sınırı, uygulamanın test ortamındaki yük toleransı, ve kurulum maliyeti. Test edilen uygulama tek instance bir staging ortamıysa shard sayısını artırmak testleri hızlandırmaz; backend’i kuyruğa sokar ve timeout üretir.

Tek makinede worker sayısını doğru seçmek
GitHub Actions standart Linux runner’ı 2 çekirdek verir. Yerel makinende 8 çekirdekle workers: 8 çalışıyor diye aynı değeri CI’ya taşırsan sekiz browser süreci iki çekirdeği paylaşır. Sonuç yavaşlama değil sadece: timeout’a yakın bekleyen assertion’lar eşiği aşmaya başlar ve suite flaky hale gelir. Kararsız testin en sık görülen altyapısal sebebi budur — flaky testleri teşhis ederken önce runner’ın yük altında olup olmadığına bakmak gerekir.
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
const isCI = !!process.env.CI;
export default defineConfig({
testDir: './tests',
// Aynı dosyadaki testler de paralel çalışır.
// Dosya içi sıraya bağımlı testin varsa bu ayar onu kırar — iyi ki kırar.
fullyParallel: true,
// CI'da yanlışlıkla commit edilmiş test.only build'i kırsın.
forbidOnly: isCI,
// Runner 2 çekirdek verir; worker başına bir browser süreci düşünüyoruz.
workers: isCI ? 2 : undefined,
retries: isCI ? 1 : 0,
timeout: 30_000,
expect: { timeout: 7_000 },
reporter: isCI
? [['blob'], ['list']]
: [['html', { open: 'never' }]],
use: {
baseURL: process.env.BASE_URL ?? 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
],
});
workers ayarını undefined bırakırsan Playwright çekirdek sayısının yarısını kullanır. CI’da bunu açıkça yazmak daha iyi: runner tipi değiştiğinde davranışın sessizce kaymasını engeller.
fullyParallel: true bir anahtar değil, bir politika. Açtığın anda aynı spec dosyasındaki testlerin birbirine sıra bağımlılığı varsa patlar. Bu kaybedilmiş bir şey değil; o bağımlılık zaten vardı, sadece görünür oldu.
Shard’ları CI matrisine bağlamak
--shard=i/n bayrağı suite’i n parçaya böler ve yalnızca i’inci parçayı çalıştırır. GitHub Actions matrix strategy bu i değerini üretmek için yeterli.
# .github/workflows/e2e.yml
name: E2E Tests
on:
pull_request:
push:
branches: [main]
jobs:
test:
name: Shard ${{ matrix.shard }}/${{ strategy.job-total }}
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Bağımlılıkları kur
run: npm ci
- name: Playwright tarayıcılarını kur
run: npx playwright install --with-deps chromium
- name: Testleri çalıştır
run: npx playwright test --shard=${{ matrix.shard }}/${{ strategy.job-total }}
env:
CI: 'true'
BASE_URL: ${{ secrets.STAGING_URL }}
- name: Blob raporu yükle
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: blob-report-${{ matrix.shard }}
path: blob-report
retention-days: 3
Üç detay önemli. fail-fast: false olmazsa bir shard kırıldığında diğerleri iptal edilir ve elinde eksik rapor kalır. strategy.job-total sayesinde matrisi 4’ten 6’ya çıkarırken komut satırını düzeltmeyi unutmazsın. if: ${{ !cancelled() }} olmadan kırmızı shard’ın blob’u yüklenmez — yani tam da görmek istediğin veri kaybolur.
Shard dengesizliği ve çözümü
Playwright shard’ları test dosyası sayısına göre böler, süreye göre değil. Dosyaların süresi birbirine eşit değilse — ki hiçbir suite’te eşit değildir — bir shard 3 dakikada biterken diğeri 12 dakika sürer. Toplam duvar saati en yavaş shard’a eşittir; diğer üç runner boş yere ücret ödeyip bekler.
Belirti nettir: matris job’larının süreleri arasında iki katından fazla fark varsa dengesizlik vardır. Bunu Actions arayüzündeki job sürelerine bakarak ya da merge edilmiş raporda her dosyanın süresini okuyarak görürsün.
Üç müdahale var, bu sırayla:
Ağır spec dosyalarını böl. Tek dosyada 40 test varsa o dosya atomiktir, bölünemez ve hangi shard’a düşerse onu yavaşlatır. Mantıksal gruplara ayırıp ayrı dosyalara dağıtmak dengelemenin en doğrudan yolu.
Süre verisine bakarak dosya dağılımını düzelt. Son çalıştırmanın JSON veya JUnit raporundan dosya başına süreyi çıkar, en uzun dosyaları farklı shard’lara denk getirecek şekilde isimlendirme veya proje ayrımı yap. Playwright dosyaları deterministik bir sırayla böldüğü için bu dağılım tekrarlanabilir.
Shard sayısını job başlatma maliyetine karşı dengele. Dengesizlik devam ediyorsa shard sayısını artırmak dilimleri küçültür ve varyansı düşürür — ama her yeni shard kurulum maliyeti ekler. Bu dengeyi bir sonraki bölümdeki orana bakarak kur.
Raporları birleştirmek: blob reporter ve merge-reports
Her shard kendi HTML raporunu üretirse elinde dört tane yarım rapor olur. Hiçbiri suite’in gerçek durumunu göstermez: toplam test sayısı yanlış, flaky listesi parçalı, “kaç test geçti” sorusunun cevabı dört artifact’i elle toplamayı gerektirir.
Blob reporter bunun için var. HTML üretmez; birleştirilebilir ham çıktı üretir. Yukarıdaki workflow’a eklenecek ikinci job şu:
merge-reports:
name: Raporları birleştir
if: ${{ !cancelled() }}
needs: [test]
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Bağımlılıkları kur
run: npm ci
- name: Blob raporları indir
uses: actions/download-artifact@v4
with:
path: all-blob-reports
pattern: blob-report-*
merge-multiple: true
- name: Tek rapora dönüştür
run: |
npx playwright merge-reports \
--reporter html,junit,github \
./all-blob-reports
- name: Birleşik raporu yayınla
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-report
retention-days: 14
merge-multiple: true dört ayrı artifact’i tek klasöre açar. --reporter html,junit,github aynı blob setinden üç çıktı üretir: insanın açıp bakacağı HTML, test raporlama araçlarının okuyacağı JUnit XML, ve PR’ın Actions sekmesinde özet gösteren github reporter. JUnit çıktısının yolu config’teki reporter ayarından değil, bu komuttan gelir — ayrı bir outputFile tanımı gerekiyorsa PLAYWRIGHT_JUNIT_OUTPUT_NAME ortam değişkenini kullan.
Birleşik HTML raporun özetinde dört sayı yer alır: passed, failed, flaky, skipped. Dikkat edilmesi gereken üçüncüsü.
Paralel çalıştırmanın ortaya çıkardığı gizli bağımlılıklar
Sıralı çalışırken yıllardır geçen testler shard’a geçtiğin gün patlar. Sebep neredeyse her zaman aynı dört şeyden biri: iki testin aynı kullanıcı hesabıyla giriş yapması, sabit bir ID’ye bağlı test verisi, aynı veritabanı kaydını güncelleyen iki senaryo, ya da modül seviyesinde tutulan global state.
Paralelleştirme bu hataları üretmez, var olanı görünür kılar. Sıralı çalışmada test A, test B’nin bıraktığı veriye güveniyordu; bu bir bağımlılıktı ama kimse farkında değildi. Her testin kendi verisini üretmesi paralel çalıştırmanın ön koşuludur, sonrasında yapılacak iyileştirme değil.
Çözüm fixture seviyesinde kurulur: her test kendi kullanıcısını API üzerinden yaratır, kendi storageState’iyle oturum açar, bittiğinde temizler.
// tests/fixtures.ts
import { test as base, expect, request } from '@playwright/test';
type Kullanici = {
email: string;
sifre: string;
id: string;
};
type Fixtures = {
kullanici: Kullanici;
};
export const test = base.extend<Fixtures>({
// Her test kendi kullanıcısını yaratır; hiçbir test başkasının verisine dokunmaz.
kullanici: async ({ baseURL }, use) => {
const api = await request.newContext({ baseURL });
const email = `test-${crypto.randomUUID()}@example.com`;
const sifre = 'Gecici!2024';
const olustur = await api.post('/api/test/users', {
data: { email, password: sifre },
});
expect(olustur.ok()).toBeTruthy();
const { id } = await olustur.json();
await use({ email, sifre, id });
await api.delete(`/api/test/users/${id}`);
await api.dispose();
},
// Oturum, o teste ait kullanıcıyla kurulur — paylaşılan storageState yok.
storageState: async ({ kullanici, baseURL }, use) => {
const api = await request.newContext({ baseURL });
await api.post('/api/login', {
data: { email: kullanici.email, password: kullanici.sifre },
});
const state = await api.storageState();
await api.dispose();
await use(state);
},
});
export { expect };
// tests/profil.spec.ts
import { test, expect } from './fixtures';
test('kullanıcı görünen adını değiştirebilir', async ({ page, kullanici }) => {
await page.goto('/profil');
await page.getByLabel('Görünen ad').fill('Yeni Ad');
await page.getByRole('button', { name: 'Kaydet' }).click();
await expect(page.getByText('Profil güncellendi')).toBeVisible();
await expect(page.getByLabel('Görünen ad')).toHaveValue('Yeni Ad');
expect(kullanici.email).toContain('@example.com');
});
Bu yapıyı kurmadan shard sayısını artırmak, hata sayısını artırmaktan başka bir şey yapmaz. Suite büyüdükçe izolasyonun nasıl korunacağı ölçeklenebilirlik tarafının ana konusu.
Retry, flaky raporlama ve yanlış güvenlik hissi
CI’da retries: 2 yazmak kırmızıyı yeşile çevirir. Build geçer, PR merge olur, kimse bir şey sormaz. Ama retry bir çözüm değil, bir erteleme: ikinci denemede geçen test, üretimde ilk denemede patlayacak davranışı işaret ediyor olabilir.
Retry’ı tamamen kaldırmak da işe yaramaz; ağ kaynaklı gerçek gürültü paylaşımlı CI runner’larında kaçınılmaz. Doğru kurulum retry’ı açık tutmak ama ücretsiz bırakmamaktır:
- Flaky’yi ayrı metrik olarak takip et. Merge edilmiş rapordaki flaky sayısı, retry’da geçen testlerin sayısıdır. Bu sayı sıfır değilse suite’te düzeltilmemiş iş var demektir.
- Haftalık gözden geçir. Hangi testler tekrar tekrar flaky listesine düşüyor? Aynı isim üçüncü haftada da görünüyorsa o test kararsız değil, yanlış yazılmış.
- Eşik koy ve build’i kır. JUnit çıktısını parse edip flaky sayısı belirlediğin eşiği aşarsa job’ı başarısız yapan bir adım ekle. Eşiksiz metrik, kimsenin bakmadığı metriktir.
Retry sayısını düşük tutmak için ek bir gerekçe daha var: her retry duvar saatini uzatır ve bu süre en yavaş shard’a eklenir. İki retry, kronik flaky bir testi olan bir suite’te shard’dan kazandığın süreyi geri alır.
Maliyet: kaç shard gerçekten mantıklı
Her shard job’ı sabit bir kurulum maliyeti öder: runner’ın ayağa kalkması, npm ci, playwright install. Bu maliyet shard sayısıyla çarpılır ama testler arasında bölünmez.
Karar için tek bir orana bak: shard başına test süresi / kurulum süresi. Kurulum iki dakika sürüyorsa ve shard başına test süresi bir dakikaya inmişse, faturanın üçte ikisini hiç test çalıştırmayan dakikalara ödüyorsun. Pratik eşik, shard başına test süresinin kurulum süresinin en az üç katı olması — bunun altında yeni shard duvar saatini anlamlı düşürmez, sadece paralel job sayını ve kuyruk riskini artırır.
Dört dakikalık bir suite’i on shard’a bölmek bu yüzden anlamsız: her shard yaklaşık yirmi dört saniye test çalıştırmak için iki dakika kurulum yapar. Toplam duvar saati kurulum süresine sabitlenir, fatura ikiye katlanır.
Kurulum maliyetini düşürmek shard sayısını artırmanın ön koşulu. actions/setup-node cache’i, browser binary’leri için actions/cache, veya hazır Playwright Docker image’ı kullanmak bu iki dakikayı ciddi biçimde kısaltır — ve eşiği aşağı çeker. Daha geniş bir optimizasyon çerçevesi için test otomasyonunda performans tarafına bakabilirsin.

Bundan sonra ne yapmalı
Sıra önemli, çünkü yanlış sırada yapılan optimizasyon ölçülemez.
- Ölç. Mevcut suite’i tek runner’da çalıştır, dosya başına süreyi JSON reporter’dan çıkar. En uzun beş dosyayı ve toplam süreyi not et. Bu senin referans çizgin.
- Worker’ı sabitle.
playwright.config.tsiçinde CI’ya özelworkersdeğerini runner’ın çekirdek sayısına göre yaz. Bu değişikliği tek başına ölç — shard eklemeden önce. - İzolasyonu kur.
fullyParallel: trueaç, kırılan testleri düzelt. Her test kendi verisini üretene kadar bir sonraki adıma geçme. - Shard ekle. İki shard’la başla, süre dağılımını kontrol et, oran eşiği izin verdiği sürece artır.
- Merge-reports’u bağla. Blob reporter ve birleştirme job’ı olmadan shard sayısını artırmak, görünürlüğü artırmaz; azaltır.
Bu sırayı bozup doğrudan shard matrisine geçersen, kırılan testlerin sebebinin paralellik mi yoksa gizli bağımlılık mı olduğunu ayırt edemezsin. Araç seçimi aşamasındaysan ve Playwright’ın bu konudaki yerini merak ediyorsan araçları gerçek kriterlerle karşılaştıran yazıya bak — shard ve blob reporter desteği bu karşılaştırmanın ağır basan kalemlerinden.