Test Araçları Rehberi

Bu projede kullanılan üç test aracının — Playwright, Hurl ve k6 — komutları, kod açıklamaları ve gerçek test örnekleriyle kapsamlı rehberi.

🎭 Playwright UI Testing
🌀 Hurl API Testing
k6 Performance Testing

Proje Yapısı

Projedeki dosyaların organizasyonu ve her klasörün amacı.

Dizin Yapısı tree
All-Test-Types/
├── playwright.config.ts     # Playwright ana konfigürasyonu
├── package.json             # npm scriptleri ve bağımlılıklar
│                             # Yalnızca Playwright buraya bağımlılık olarak eklenir;
│                             # Hurl ve k6 bağımsız CLI araçlarıdır — npm ekosistemiyle
│                             # ilişkileri yoktur. Her ikisi de işletim sistemi düzeyinde
│                             # (brew install hurl / brew install k6 veya resmi binary
│                             # indirilerek) kurulur, dolayısıyla package.json'a girmeleri
│                             # gerekmez ve npx/node_modules ile çalışmazlar.
├── guide.html               # Bu rehber
│
├── tests/
│   ├── web/                 # Playwright UI testleri
│   │   ├── pages/           # Page Object Model sınıfları
│   │   │   ├── BasePage.ts
│   │   │   ├── PlaywrightHomePage.ts
│   │   │   ├── PlaywrightDocsPage.ts
│   │   │   └── JsonPlaceholderPage.ts
│   │   ├── homepage.spec.ts     # Temel sayfa testleri
│   │   ├── form.spec.ts         # Form/input testleri
│   │   ├── api-mock.spec.ts     # Network intercept testleri
│   │   ├── pom-homepage.spec.ts # POM ile homepage testleri
│   │   ├── pom-docs.spec.ts     # POM ile docs testleri
│   │   └── pom-api.spec.ts      # POM ile API + mock testleri
│   │
│   ├── api/                 # Hurl API testleri
│   │   ├── posts.hurl           # CRUD endpoint testleri
│   │   ├── users.hurl           # Kullanıcı endpoint testleri
│   │   └── auth.hurl            # Auth/header testleri
│   │
│   └── performance/         # k6 yük testleri
│       ├── load-test.js         # Normal yük testi
│       ├── stress-test.js       # Stres testi
│       ├── spike-test.js        # Spike testi
│       └── summary.js           # Rapor oluşturucu
│
├── scripts/
│   ├── run-all.sh               # Tüm testleri bash ile çalıştır
│   └── k6-runner.mjs            # Tüm k6 testlerini sırayla çalıştır
│
└── reports/                 # Üretilen raporlar (gitignore)
    ├── playwright/
    ├── playwright-results/
    ├── api/
    └── k6/

Araç Ekosistemi Mimarisi

Her araç farklı bir runtime üzerinde çalışır — bu yüzden yalnızca Playwright package.json'a girer.

🎭 Playwright Node.js
Test Dosyaları
tests/web/*.spec.ts
↓ require()
@playwright/test
node_modules/ — npm install
↓ çalıştırır
Node.js Runtime
V8 engine · async/await
↓ kontrol eder
Browser Engines
Chromium · Firefox · WebKit
✅ package.json gerekir
🔁 Hurl Rust Binary
Test Dosyaları
tests/api/*.hurl
↓ hurl komutu okur
hurl binary
/usr/local/bin/hurl
↓ Rust ile derlendi
Rust Runtime
Statik derleme · sıfır bağımlılık
↓ HTTP/TLS isteği
Hedef API / Sunucu
curl tabanlı HTTP istemcisi
⛔ package.json gerekmez
⚡ k6 Go Binary
Test Dosyaları
tests/performance/*.js
↓ k6 komutu okur
k6 binary
/usr/local/bin/k6
↓ Go ile derlendi
Go Runtime + Goja VM
JS parse eder · Node.js DEĞİL
↓ binlerce goroutine
Hedef API / Sunucu
HTTP yük testi — VU simülasyonu
⛔ package.json gerekmez
AraçRuntimeKurulumpackage.jsonDosya Formatı
Playwright Node.js (V8) npm install ✅ Gerekir .spec.ts / .spec.js
Hurl Rust (statik binary) brew install hurl ⛔ Gerekmez .hurl
k6 Go + Goja (JS VM) brew install k6 ⛔ Gerekmez .js (ES module)
k6 JavaScript çalıştırıyor ama Node.js değil! k6 test dosyaları .js uzantılıdır ve ES module söz dizimiyle (import/export) yazılır. Ancak bunları çalıştıran Node.js değil, k6'nın içindeki Goja adlı Go tabanlı JS motorudur. Bu yüzden require() veya node_modules kullanılamaz; yalnızca k6/* modülleri desteklenir.
Web Testleri
20
6 spec dosyasında Playwright testi
API Testleri
12
3 Hurl dosyasında endpoint testi
Perf. Testleri
3
Load, Stress, Spike senaryoları
POM Sınıfları
4
BasePage dahil Page Object sınıfı

Tüm Komutlar Quick Ref

Projede kullanabileceğin tüm terminal komutları.

npm Scriptleri

npm test

Playwright → Hurl → k6 sırasıyla tüm testleri çalıştırır

bash scripts/run-all.sh

Bash script ile tüm testleri çalıştırır, raporları üretir

npm run test:web

Tüm Playwright testlerini headless modda çalıştırır

npm run test:web:headed

Playwright'ı tarayıcı penceresini göstererek çalıştırır

npm run test:web:ui

Playwright UI Test Runner'ı açar (interaktif mod)

npm run test:web:debug

Playwright Inspector ile adım adım debug modu

npm run test:api

Tüm .hurl dosyalarını glob ile çalıştırır

npm run test:api:verbose

Hurl testlerini verbose (detaylı HTTP log) modda çalıştırır

npm run test:api:report

Hurl testlerini çalıştırır ve HTML rapor üretir

npm run test:perf

k6-runner.mjs ile load/stress/spike testlerini sırayla çalıştırır

npm run report

Playwright HTML raporunu tarayıcıda açar

Playwright — Direkt CLI

npx playwright test --grep "@smoke"

Sadece @smoke etiketli testleri çalıştırır

npx playwright test --grep "@smoke|@critical"

@smoke VEYA @critical etiketli testleri çalıştırır

npx playwright test --grep-invert "@regression"

@regression dışındaki tüm testleri çalıştırır

npx playwright test homepage.spec.ts

Sadece homepage.spec.ts dosyasını çalıştırır

npx playwright test --project=chromium

Sadece Chromium'da çalıştırır

npx playwright test --workers=4

4 paralel worker ile testleri hızlandırır

npx playwright codegen https://example.com

Tarayıcıda gezinerek otomatik test kodu üretir

npx playwright show-report

En son HTML test raporunu tarayıcıda açar

Hurl — Direkt CLI

hurl --test tests/api/posts.hurl

Tek bir .hurl dosyasını test modunda çalıştırır

hurl --test --glob "tests/api/**/*.hurl"

Glob pattern ile tüm hurl dosyalarını çalıştırır

hurl --test --verbose tests/api/auth.hurl

HTTP istek/yanıt detaylarını göstererek çalıştırır

hurl --report-html reports/api posts.hurl

Çalıştırır ve HTML rapor üretir

k6 — Direkt CLI

k6 run tests/performance/load-test.js

Load testini dosyadaki konfigürasyonla çalıştırır

k6 run --vus 20 --duration 60s load-test.js

VU ve süreyi override ederek çalıştırır

k6 run --out json=reports/k6/out.json load-test.js

Sonuçları JSON dosyasına yazar

node scripts/k6-runner.mjs

Tüm k6 testlerini sırayla çalıştırır, raporları üretir

🎭 Playwright UI Testing

Playwright, modern web uygulamalarını Chromium, Firefox ve WebKit'te end-to-end test etmek için kullanılan güçlü bir framework'tür.

Test Dosyası Anatomisi

Bir Playwright test dosyasının her parçasının ne anlama geldiğini inceleyelim:

tests/web/homepage.spec.ts — Satır satır açıklama TypeScript
// ① Playwright'ın test runner'ını ve assertion kütüphanesini import et
import { test, expect } from '@playwright/test';

// ② test.describe → birden fazla testi mantıksal olarak gruplar
//    İlk argüman: grup adı (raporlarda görünür)
test.describe('Playwright Ana Sayfa', () => {

  // ③ test() → tek bir test case tanımlar
  //    İlk argüman: test adı + etiketler (tag sistemi için)
  //    İkinci argüman: async callback, { page } ile tarayıcı sayfasını alır
  test('sayfa başlığı doğru yükleniyor @smoke @critical', async ({ page }) => {

    // ④ page.goto() → belirtilen URL'e gider
    //    baseURL playwright.config.ts'de tanımlı olduğunda otomatik eklenir
    await page.goto('https://playwright.dev');

    // ⑤ expect() → bir değeri veya locator'ı test eder
    //    toHaveTitle() → sayfanın <title> tag'ini kontrol eder
    //    /regex/ veya string kabul eder
    await expect(page).toHaveTitle(/Playwright/);
  });

  test('ana navigasyon görünür @smoke @ui', async ({ page }) => {
    await page.goto('https://playwright.dev');

    // ⑥ page.locator() → CSS/XPath ile element seçer
    // ⑦ toBeVisible() → elementin DOM'da görünür olduğunu kontrol eder
    await expect(page.locator('nav')).toBeVisible();
  });

  test('docs linkine tıklanabiliyor @regression @ui', async ({ page }) => {
    await page.goto('https://playwright.dev');

    // ⑧ getByRole() → ARIA rolüne göre element bulur (erişilebilirlik ağacı)
    //    'link' rolü → <a> tag'leri, name: linkın içindeki metin
    await page.getByRole('link', { name: 'Docs' }).click();

    // ⑨ toHaveURL() → mevcut URL'in beklenen değerle eşleşip eşleşmediğini kontrol eder
    await expect(page).toHaveURL(/docs/);
  });
});

Locator Stratejileri

Playwright'ta element bulmak için birden fazla yöntem vardır. Öncelik sırasına göre:

Yöntem Kullanım Ne zaman kullanılır?
getByRole() page.getByRole('button', { name: 'Submit' }) ARIA rolüne göre — en güvenilir yöntem
getByText() page.getByText('Merhaba') Görünen metin içeriğine göre
getByLabel() page.getByLabel('Email') Form label'ına bağlı input alanları
getByPlaceholder() page.getByPlaceholder('Ara...') Placeholder attr'una sahip input'lar
getByTestId() page.getByTestId('submit-btn') data-testid attr'u ile — test için eklenmiş
locator() page.locator('nav > ul > li') CSS selector veya XPath ile

Playwright Konfigürasyonu

playwright.config.ts — Her alanın açıklaması TypeScript
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({

  // testDir: Test dosyalarının aranacağı kök dizin
  testDir: './tests/web',

  // outputDir: Screenshot, video, trace gibi test artifact'larının kaydedileceği yer
  outputDir: './reports/playwright-results',

  // reporter: Hangi raporlayıcıların kullanılacağı (birden fazla olabilir)
  reporter: [
    ['list'],                                              // Terminal'de satır satır sonuç
    ['html', { outputFolder: './reports/playwright', open: 'never' }], // HTML rapor
  ],

  use: {
    // baseURL: page.goto('/path') çağrılarında otomatik ön ek olarak eklenir
    baseURL: 'https://jsonplaceholder.typicode.com',

    // trace: İlk başarısız denemede trace kaydeder (hata ayıklamak için)
    // 'on' | 'off' | 'on-first-retry' | 'retain-on-failure'
    trace: 'on-first-retry',

    // screenshot: Sadece başarısız testlerde ekran görüntüsü alır
    // 'on' | 'off' | 'only-on-failure'
    screenshot: 'only-on-failure',
  },

  // projects: Farklı tarayıcı/cihaz kombinasyonlarını tanımlar
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },  // Hazır cihaz profili kullanır
    },
    // İlave projeler kolayca eklenir:
    // { name: 'firefox',      use: { ...devices['Desktop Firefox'] } },
    // { name: 'mobile-safari', use: { ...devices['iPhone 13'] } },
  ],
});

form.spec.ts — Gelişmiş Etkileşimler

tests/web/form.spec.ts TypeScript
import { test, expect } from '@playwright/test';

test.describe('Form ve Input Testleri', () => {

  test('arama formu çalışıyor @smoke @ui', async ({ page }) => {
    await page.goto('https://playwright.dev');

    // Butona tıkla — arama modalını aç
    await page.getByRole('button', { name: 'Search' }).click();

    // Açılan input'a metin yaz
    await page.keyboard.type('page');

    // İçinde 'page' geçen sonuçların göründüğünü doğrula
    await expect(page.getByRole('option').first()).toBeVisible();
  });

  test('viewport ve responsive kontrol @regression @ui', async ({ page }) => {
    // Tarayıcı penceresini 375×812 piksel (iPhone boyutu) yap
    await page.setViewportSize({ width: 375, height: 812 });
    await page.goto('https://playwright.dev');

    // Masaüstü navigasyonun mobilde gizlendiğini doğrula
    await expect(page.locator('.navbar__items--right')).toBeHidden();
  });
});
Sık Kullanılan Assertion'lar toBeVisible() — element görünür mü · toBeHidden() — element gizli mi · toHaveText() — metin içeriği eşleşiyor mu · toHaveValue() — input değeri eşleşiyor mu · toBeEnabled() — element aktif mi

Page Object Model (POM)

POM, sayfa elementlerini ve onlarla yapılan etkileşimleri ayrı sınıflara taşıyarak test kodunu daha temiz ve bakımı kolay hale getirir.

tests/web/pages/BasePage.ts — Temel sınıf TypeScript
import { Page } from '@playwright/test';

// Tüm POM sınıflarının miras aldığı temel sınıf
export class BasePage {
  protected readonly page: Page;

  // Constructor: Playwright'ın page nesnesini alır ve saklar
  constructor(page: Page) {
    this.page = page;
  }

  // Ortak metodlar — tüm alt sınıflar kullanabilir
  async navigate(path: string): Promise<void> {
    await this.page.goto(path);
  }

  async getTitle(): Promise<string> {
    return await this.page.title();
  }

  async waitForNetworkIdle(): Promise<void> {
    // 500ms boyunca yeni network isteği başlamamasını bekler
    await this.page.waitForLoadState('networkidle');
  }

  async getURL(): Promise<string> {
    return this.page.url();
  }
}
tests/web/pages/PlaywrightHomePage.ts — Sayfa sınıfı TypeScript
import { Page, Locator } from '@playwright/test';
import { BasePage } from './BasePage';

export class PlaywrightHomePage extends BasePage {

  // readonly Locator'lar: sınıf oluşturulduğunda tanımlanır, değişmez
  // Bu elementler test boyunca tekrar tekrar kullanılabilir
  readonly navbar: Locator;
  readonly docsLink: Locator;
  readonly searchButton: Locator;

  constructor(page: Page) {
    super(page); // BasePage'i başlatır

    // Locator'ları burada tanımla — lazy evaluation: henüz DOM'a bakmaz
    this.navbar = page.locator('nav.navbar');
    this.docsLink = page.getByRole('link', { name: 'Docs' });
    this.searchButton = page.getByRole('button', { name: 'Search' });
  }

  // Sayfaya özel metodlar — testte tek satırla karmaşık işlem yapılır
  async goto() {
    await this.navigate('https://playwright.dev');
  }

  async clickDocs() {
    await this.docsLink.click();
  }

  async searchFor(term: string) {
    await this.searchButton.click();
    await this.page.keyboard.type(term);
  }

  async isNavbarVisible(): Promise<boolean> {
    return await this.navbar.isVisible();
  }
}
tests/web/pom-homepage.spec.ts — POM kullanımı TypeScript
import { test, expect } from '@playwright/test';
import { PlaywrightHomePage } from './pages/PlaywrightHomePage';

test.describe('POM ile Ana Sayfa Testleri', () => {

  test('sayfa başlığı doğru @smoke @critical', async ({ page }) => {
    // POM nesnesini oluştur — page fixture'ı geçir
    const homePage = new PlaywrightHomePage(page);

    // Metodu çağır — goto() içinde navigate() BasePage'den miras alır
    await homePage.goto();

    // POM metodunu kullanarak assertion yap
    await expect(page).toHaveTitle(/Playwright/);
  });

  test('docs sayfasına yönlendiriyor @regression @ui', async ({ page }) => {
    const homePage = new PlaywrightHomePage(page);
    await homePage.goto();

    // clickDocs() → locator tanımı ve click() mantığını kapsüller
    await homePage.clickDocs();

    await expect(page).toHaveURL(/docs/);
  });
});
  • 1
    BasePage — Ortak Davranışlar

    navigate(), getTitle(), waitForNetworkIdle() gibi tüm sayfalarda kullanılacak metodlar buraya taşınır.

  • 2
    Sayfa Sınıfı — Locator'lar + Metodlar

    Her sayfanın elementleri readonly Locator olarak, etkileşimleri metod olarak tanımlanır.

  • 3
    Spec Dosyası — Sadece Test Mantığı

    Testler POM metodlarını çağırır; selector'lar veya implementation detayları içermez.

Network Intercept ve API Mock

tests/web/api-mock.spec.ts — Network yakalama ve mock TypeScript
import { test, expect } from '@playwright/test';

test('network isteği yakalanıyor @regression @network', async ({ page }) => {
  let intercepted = false;

  // page.on('request') → sayfadan çıkan HER isteği dinler
  // Event listener olduğu için await gerekmez
  page.on('request', (req) => {
    if (req.url().includes('jsonplaceholder')) {
      intercepted = true;
    }
  });

  // baseURL'den /posts/1'e git → XHR/fetch isteği tetikler
  await page.goto('/posts/1');

  expect(intercepted).toBe(true);
});

test('API yanıtı mock ediliyor @smoke @network', async ({ page }) => {

  // page.route() → belirli URL pattern'lerine gelen istekleri yakalar ve değiştirir
  // '**' globbing desteği var: '**/posts/1' → herhangi bir origin + /posts/1
  await page.route('**/posts/1', async (route) => {

    // route.fulfill() → isteği gerçeğe göndermeden sahte yanıt döner
    await route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify({
        id: 1,
        title: 'Mock Post Başlığı',
        body: 'Mock içerik',
        userId: 1
      })
    });
  });

  await page.goto('/posts/1');

  // Sayfadaki içeriğin mock datayı yansıttığını doğrula
  const body = await page.evaluate(() => document.body.innerText);
  expect(body).toContain('Mock Post Başlığı');
});
route.fulfill() vs route.continue() route.fulfill() → isteği tamamen taklit eder, asla network'e gitmez. route.continue() → isteği gerçekten gönderir ama headers/body gibi şeyleri değiştirmenize izin verir.

API Request Testi — Playwright ile HTTP İstekleri

Playwright sadece UI testi değil, doğrudan HTTP istekleri göndererek API testleri de yapabilir. Tarayıcı açmadan REST endpoint'lerini test etmek için iki farklı yol sunar.

Yöntem Ne zaman kullanılır? Fixture
request fixture Bağımsız API testleri — tarayıcı gerekmez { request }
page.request UI testi içinde API çağrısı — browser cookie/session paylaşır { page }
APIRequestContext baseURL + header gibi ayarları merkezi yönetmek için manuel oluşturulur

1 — request Fixture ile Bağımsız API Testi

Playwright'ın request fixture'ı tarayıcı olmadan HTTP istekleri göndermenizi sağlar. Hurl'e benzer ama TypeScript ile tam tip desteğiyle.

request fixture — GET, POST, PUT, DELETE tam CRUD örneği TypeScript
import { test, expect } from '@playwright/test';

// { request } → tarayıcı açılmaz, sadece HTTP context başlar
test.describe('Playwright API Testleri — request fixture', () => {

  // ── GET — Tek kayıt ─────────────────────────────────────────
  test('GET /posts/1 — post detayı geliyor @smoke @network', async ({ request }) => {

    // request.get() → GET isteği gönderir, APIResponse döner
    const response = await request.get(
      'https://jsonplaceholder.typicode.com/posts/1'
    );

    // toBeOK() → status code 200-299 aralığında mı kontrol eder
    await expect(response).toBeOK();

    // response.status() → HTTP status code'u döner
    expect(response.status()).toBe(200);

    // response.json() → body'yi parse ederek JS objesine çevirir
    const body = await response.json();
    expect(body.id).toBe(1);
    expect(body.userId).toBeDefined();
    expect(typeof body.title).toBe('string');
  });

  // ── GET — Liste + uzunluk kontrolü ──────────────────────────
  test('GET /posts — 100 post dönüyor @regression @network', async ({ request }) => {
    const response = await request.get(
      'https://jsonplaceholder.typicode.com/posts'
    );
    await expect(response).toBeOK();

    const posts = await response.json();
    expect(Array.isArray(posts)).toBe(true);
    expect(posts).toHaveLength(100);

    // Her elemanın beklenen alanları olduğunu kontrol et
    expect(posts[0]).toMatchObject({
      id: expect.any(Number),
      title: expect.any(String),
      body: expect.any(String),
      userId: expect.any(Number),
    });
  });

  // ── POST — Yeni kayıt oluştur ────────────────────────────────
  test('POST /posts — yeni post oluşturuluyor @smoke @network', async ({ request }) => {
    const response = await request.post(
      'https://jsonplaceholder.typicode.com/posts',
      {
        // data: obje verince otomatik JSON.stringify + Content-Type: application/json
        data: {
          title: 'Playwright API Test',
          body: 'request fixture ile yazılmış test',
          userId: 1,
        },
      }
    );

    // Başarılı oluşturma → 201 Created
    expect(response.status()).toBe(201);

    const created = await response.json();
    expect(created.id).toBeDefined();     // API yeni ID atamış mı?
    expect(created.title).toBe('Playwright API Test');
    expect(created.userId).toBe(1);
  });

  // ── PUT — Kaydı güncelle ─────────────────────────────────────
  test('PUT /posts/1 — post güncelleniyor @regression @network', async ({ request }) => {
    const response = await request.put(
      'https://jsonplaceholder.typicode.com/posts/1',
      {
        data: {
          id: 1,
          title: 'Güncellenmiş Başlık',
          body: 'Güncellenmiş içerik',
          userId: 1,
        },
      }
    );
    await expect(response).toBeOK();

    const updated = await response.json();
    expect(updated.title).toBe('Güncellenmiş Başlık');
  });

  // ── PATCH — Kısmi güncelleme ─────────────────────────────────
  test('PATCH /posts/1 — sadece başlık değişiyor @regression @network', async ({ request }) => {
    // patch() → tüm kaydı değil, sadece gönderilen alanları günceller
    const response = await request.patch(
      'https://jsonplaceholder.typicode.com/posts/1',
      { data: { title: 'Sadece Başlık Değişti' } }
    );
    await expect(response).toBeOK();

    const patched = await response.json();
    expect(patched.title).toBe('Sadece Başlık Değişti');
    // Diğer alanlar hâlâ mevcut olmalı
    expect(patched.userId).toBeDefined();
  });

  // ── DELETE — Kaydı sil ───────────────────────────────────────
  test('DELETE /posts/1 — post siliniyor @regression @network', async ({ request }) => {
    // delete() → reserved keyword olduğu için Playwright'ta fetch() ile sarılır
    const response = await request.delete(
      'https://jsonplaceholder.typicode.com/posts/1'
    );
    // Silme başarılı → 200 veya 204 döner
    expect([200, 204]).toContain(response.status());
  });

  // ── 404 Error — Var olmayan kaynak ───────────────────────────
  test('GET /posts/9999 — 404 dönüyor @regression @network', async ({ request }) => {
    const response = await request.get(
      'https://jsonplaceholder.typicode.com/posts/9999'
    );
    expect(response.status()).toBe(404);
  });
});

2 — Header, Auth Token ve Query Parametre Gönderme

Headers, Bearer token, query params ile istek TypeScript
test('Authorization header ile istek @smoke @network', async ({ request }) => {
  const response = await request.get(
    'https://jsonplaceholder.typicode.com/posts/1',
    {
      // headers: isteğe özel HTTP header'ları ekler
      headers: {
        'Authorization': 'Bearer my-token-123',
        'Accept': 'application/json',
        'X-Custom-Header': 'test-value',
      },
    }
  );
  await expect(response).toBeOK();
});

test('Query parametreli istek @smoke @network', async ({ request }) => {
  const response = await request.get(
    'https://jsonplaceholder.typicode.com/posts',
    {
      // params: objeyi otomatik URL query string'e dönüştürür
      // → /posts?userId=1&_limit=5 olur
      params: {
        userId: 1,
        _limit: 5,
      },
    }
  );
  const posts = await response.json();
  expect(posts).toHaveLength(5);
  // Tüm sonuçlar userId=1'e ait mi?
  posts.forEach((p: any) => expect(p.userId).toBe(1));
});

test('Response header kontrolü @regression @network', async ({ request }) => {
  const response = await request.get(
    'https://jsonplaceholder.typicode.com/posts/1'
  );

  // response.headers() → tüm response header'larını obje olarak döner
  const headers = response.headers();
  expect(headers['content-type']).toContain('application/json');

  // response.headersArray() → [{ name, value }] formatında döner
  const headersArr = response.headersArray();
  const ct = headersArr.find((h) => h.name === 'content-type');
  expect(ct?.value).toContain('application/json');
});

3 — page.request ile UI + API Karma Testi

Projemizde JsonPlaceholderPage.ts'de kullanılan yöntem budur. page.request, tarayıcının cookie ve session bilgilerini paylaşır — login gerektiren endpoint testleri için idealdir.

tests/web/pages/JsonPlaceholderPage.ts — page.request kullanımı TypeScript
import { Page } from '@playwright/test';
import { BasePage } from './BasePage';

export class JsonPlaceholderPage extends BasePage {
  readonly url = 'https://jsonplaceholder.typicode.com';

  async fetchPost(id: number) {
    // this.page.request.get() → page'in request context'ini kullanır
    // Fark: page cookie'lerini, session'ı paylaşır — login sonrası testler için ideal
    const response = await this.page.request.get(`${this.url}/posts/${id}`);
    return response.json(); // Doğrudan objeyi döner
  }

  async fetchUser(id: number) {
    const response = await this.page.request.get(`${this.url}/users/${id}`);
    return response.json();
  }

  async createPost(payload: { title: string; body: string; userId: number }) {
    const response = await this.page.request.post(`${this.url}/posts`, {
      data: payload, // data: {} → Content-Type: application/json otomatik eklenir
    });
    // Hem status hem de body'yi döndür — test katmanında ikisini de kontrol et
    return { status: response.status(), body: await response.json() };
  }
}
tests/web/pom-api.spec.ts — POM üzerinden API + Mock testleri TypeScript
import { test, expect } from '@playwright/test';
import { JsonPlaceholderPage } from './pages/JsonPlaceholderPage';

test.describe('POM — JsonPlaceholder API & Mock', () => {
  let apiPage: JsonPlaceholderPage;

  // beforeEach: Her testten önce POM nesnesini taze oluştur
  test.beforeEach(async ({ page }) => {
    apiPage = new JsonPlaceholderPage(page);
  });

  test('post verisi doğru geliyor @smoke @network', async () => {
    const post = await apiPage.fetchPost(1);

    expect(post.id).toBe(1);
    expect(post.userId).toBeDefined();
    expect(typeof post.title).toBe('string');
  });

  test('yeni post oluşturuluyor @regression @network', async () => {
    const { status, body } = await apiPage.createPost({
      title: 'POM Test Post',
      body: 'POM ile yazılmış test.',
      userId: 1,
    });
    expect(status).toBe(201);
    expect(body.title).toBe('POM Test Post');
    expect(body.userId).toBe(1);
  });

  test('API yanıtı mock ediliyor @smoke @network', async ({ page }) => {
    // POM'daki mockGetPost → page.route() kullanarak yanıtı taklit eder
    await apiPage.mockGetPost(1, { id: 1, title: 'Mock Başlık', userId: 99 });

    // Sayfaya git — route() aktif olduğu için gerçek API'ye gidilmez
    await page.goto('https://jsonplaceholder.typicode.com/posts/1');

    const text = await page.textContent('body');
    const data = JSON.parse(text!);

    expect(data.title).toBe('Mock Başlık');
    expect(data.userId).toBe(99);  // Gerçek API'deki 1 değil, mock'taki 99
  });
});

4 — APIRequestContext ile Merkezi Konfigürasyon

Aynı baseURL ve header'ları tüm testlerde tekrar yazmak yerine merkezi bir context oluşturabilirsin.

APIRequestContext — baseURL ve header'ları bir kez tanımla TypeScript
import { test, expect, request as playwrightRequest } from '@playwright/test';

test.describe('APIRequestContext ile Merkezi Ayarlar', () => {
  let apiContext: any;

  // beforeAll: Tüm testlerden önce tek bir context oluştur
  test.beforeAll(async () => {
    apiContext = await playwrightRequest.newContext({
      // baseURL: Tüm isteklerde otomatik ön ek kullanılır
      baseURL: 'https://jsonplaceholder.typicode.com',

      // extraHTTPHeaders: Her istekte gönderilecek ortak header'lar
      extraHTTPHeaders: {
        'Authorization': 'Bearer token-abc',
        'Accept': 'application/json',
        'X-App-Version': '1.0.0',
      },
    });
  });

  // afterAll: Context'i kapat, kaynakları serbest bırak
  test.afterAll(async () => {
    await apiContext.dispose();
  });

  test('posts endpoint @smoke @network', async () => {
    // baseURL tanımlı → sadece path yeterli: '/posts/1'
    const res = await apiContext.get('/posts/1');
    await expect(res).toBeOK();
    const post = await res.json();
    expect(post.id).toBe(1);
  });

  test('users endpoint @smoke @network', async () => {
    const res = await apiContext.get('/users/1');
    const user = await res.json();
    expect(user.name).toBe('Leanne Graham');
  });
});

5 — UI Testi İçinde API Çağrısı (Setup / Teardown)

UI testinden önce API ile test verisi oluşturmak veya sonrasında temizlemek için kullanılan yaygın bir pattern.

UI testinden önce API ile veri hazırlama TypeScript
test('UI testi — öncesinde API ile veri hazırla @regression @ui', async ({ page, request }) => {

  // ── SETUP: API ile test verisi oluştur ───────────────────────
  // { page } ve { request } aynı anda kullanılabilir
  const createRes = await request.post(
    'https://jsonplaceholder.typicode.com/posts',
    { data: { title: 'UI Test İçin Hazırlanan Post', userId: 1, body: '...' } }
  );
  expect(createRes.status()).toBe(201);
  const { id: newPostId } = await createRes.json();

  // ── TEST: Tarayıcıda oluşturulan kaydı göster ────────────────
  await page.goto(`https://jsonplaceholder.typicode.com/posts/${newPostId}`);

  // Sayfanın beklenen içeriği gösterdiğini doğrula
  const content = await page.textContent('body');
  expect(content).toBeTruthy();

  // ── TEARDOWN: API ile test verisini temizle ──────────────────
  const deleteRes = await request.delete(
    `https://jsonplaceholder.typicode.com/posts/${newPostId}`
  );
  expect([200, 204]).toContain(deleteRes.status());
});

APIResponse Metodları — Özet

Metod Dönüş Tipi Açıklama
response.status() number HTTP durum kodu (200, 201, 404 vb.)
response.ok() boolean Status 200-299 arasında mı?
response.json() Promise<any> Body'yi JSON olarak parse eder
response.text() Promise<string> Body'yi düz metin olarak döner
response.body() Promise<Buffer> Body'yi Buffer (binary) olarak döner
response.headers() Object Tüm response header'larını obje olarak döner
response.headersArray() Array [{ name, value }] formatında header'lar
response.url() string İsteğin gönderildiği son URL (redirect sonrası)

request vs page.request — Ne Farkı Var?

İki yöntemin farkı TypeScript
// ── request fixture ───────────────────────────────────────────
// • Playwright test'in kendi HTTP context'i
// • Tarayıcı cookie/session'ından bağımsız — izole API testi
// • Tarayıcı açılmaz → daha hızlı
// • Bağımsız API doğrulaması için tercih edilir
test('request fixture örneği', async ({ request }) => {
  const res = await request.get('https://api.example.com/data');
  await expect(res).toBeOK();
});

// ── page.request ──────────────────────────────────────────────
// • Tarayıcı page nesnesiyle birlikte gelir
// • Tarayıcının cookie'lerini, localStorage'ını paylaşır
// • Login sonrası oturum gerektiren endpoint'ler için ideal
// • UI testi içinde API çağrısı yapmak için kullanılır
test('page.request örneği', async ({ page }) => {
  // Kullanıcı login oldu, artık cookie var
  await page.goto('/login');
  await page.fill('#email', 'user@test.com');
  await page.fill('#password', 'pass');
  await page.click('button[type=submit]');

  // Login cookie'si ile korumalı API endpoint'i test et
  const res = await page.request.get('/api/profile');
  await expect(res).toBeOK(); // Cookie olmadan 401 dönerdi
});
Playwright API testi ne zaman tercih edilmeli? UI ve API testlerini aynı dosyada birleştirmek istiyorsan, TypeScript tip güvenliği istiyorsan veya UI testi öncesi/sonrası API ile kurulum/temizlik yapıyorsan Playwright tercih et. Sadece API test edeceksen Hurl daha kısa ve okunabilirdir.

6 — Gerçek Dünya REST API Testi — GET → PUT Önce/Sonra Pattern

Her HTTP metodunun açıklamalı, pratik bir örneğini içerir. Özellikle PUT testindeki "önce veriyi çek, sonra güncelle" pattern'i gerçek senaryolarda çok kullanılır.

api-test-0.spec.js — GET / POST / PUT / DELETE JavaScript
import { test, expect } from '@playwright/test';

// ── GET ─────────────────────────────────────────────────────────
test('GET - Kullanıcıdan Veri Çekme', async ({ request }) => {

  // 1. İsteği gönder → endpoint: /posts/1
  const response = await request.get('https://jsonplaceholder.typicode.com/posts/1');

  // 2. Status kod kontrolü — 200 OK bekleniyor
  expect(response.status()).toBe(200);

  // 3. Yanıt gövdesini JSON'a çevir — Playwright bunu otomatik yapmaz
  const body = await response.json();

  // 4. Alan değerlerini doğrula
  expect(body.id).toBe(1);
  expect(body.userId).toBe(1);
  // toBeTruthy() → değer null, undefined, '', 0, false değilse geçer
  // "Başlık dolu mu?" kontrolü için idealdir
  expect(body.title).toBeTruthy();
});

// ── POST ────────────────────────────────────────────────────────
test('POST - Yeni Kayıt Oluşturma', async ({ request }) => {

  // Gönderilecek veriyi önceden hazırla — okunabilirlik için iyi pratik
  const yeniVeri = {
    title: 'Playwright ile API Testi',
    body: 'Bu post isteği ile oluşturuldu.',
    userId: 101
  };

  const response = await request.post(
    'https://jsonplaceholder.typicode.com/posts',
    { data: yeniVeri }  // data: obje → otomatik JSON.stringify + Content-Type: application/json
  );

  // POST başarılıysa 201 Created döner (200 değil!)
  expect(response.status()).toBe(201);

  const body = await response.json();
  // JSONPlaceholder yeni kayda hep 101 ID atar
  expect(body.id).toBe(101);
  expect(body.title).toBe('Playwright ile API Testi');
});

// ── PUT — Önce Çek, Sonra Güncelle Pattern ──────────────────────
test('PUT - Mevcut Kaydı Güncelleme', async ({ request }) => {

  // Adım 1: Güncellemeden ÖNCE mevcut veriyi çek ve logla
  // Bu pattern, hangi değerlerin değiştiğini doğrulamak için kullanılır
  const response1 = await request.get('https://jsonplaceholder.typicode.com/posts/1');
  expect(response1.status()).toBe(200);
  const mevcutVeri = await response1.json();
  console.log('Güncelleme Öncesi Veri:', mevcutVeri);

  // Adım 2: Tam veriyi (id dahil) PUT body'sine ekle
  // PUT = tüm kaydı değiştir; id'yi de göndermek iyi pratiktir
  const guncelVeri = {
    id: 1,
    title: 'Güncellenmiş Başlık',
    body: 'İçerik tamamen değişti.',
    userId: 1
  };

  // URL'in sonundaki '/1' → hangi kaydın güncelleneceğini belirtir
  const response = await request.put(
    'https://jsonplaceholder.typicode.com/posts/1',
    { data: guncelVeri }
  );

  // PUT başarılıysa 200 döner
  expect(response.status()).toBe(200);

  const body = await response.json();
  expect(body.title).toBe('Güncellenmiş Başlık');
});

// ── DELETE ──────────────────────────────────────────────────────
test('DELETE - Kayıt Silme', async ({ request }) => {

  const response = await request.delete('https://jsonplaceholder.typicode.com/posts/1');

  // JSONPlaceholder silme için 200 döner
  // Gerçek API'lerin çoğu 204 No Content döner (body yok)
  expect(response.status()).toBe(200);

  const body = await response.json();
  // JSONPlaceholder silinen kayıt için boş obje {} döner
  console.log(response.status() === 200 ? 'Kayıt başarıyla silindi.' : 'Silme başarısız.');
});
POST 201, PUT/DELETE 200 — Neden farklı? 201 Created → sunucu yeni bir kaynak oluşturdu. 200 OK → mevcut bir kaynağı değiştirdi veya sildi. 204 No Content → işlem başarılı ama dönecek bir body yok (DELETE için yaygın). Bu farkı test assertion'larına yansıtmak önemlidir.

7 — GraphQL API Testi

GraphQL, REST'ten farklı olarak tek bir endpoint üzerinden çalışır. Her istek bir POST isteğidir ve query alanı içeren bir JSON body gönderilir. Playwright'ın request.post() metoduyla doğrudan test edilebilir.

GraphQL vs REST — Temel Fark REST'te her kaynak için farklı bir URL vardır (/posts, /users). GraphQL'de tek bir URL vardır ve ne istediğini query body'sinde belirtirsin. Her iki durumda da Playwright'ta kullanılan metod aynıdır: request.post(url, { data: { query } }).
continents.test.js — Liste ve dil sorgusu JavaScript
import { test, expect } from '@playwright/test';

// GraphQL'de tek endpoint — tüm sorgular buraya POST gönderir
const url = 'https://countries.trevorblades.com/';

test.describe('Continents GraphQL Tests', () => {

  // ── Test 1: Kıtaları listele ─────────────────────────────────
  test('should fetch list of continents', async ({ request }) => {

    // GraphQL query: template literal ile yazılır
    // Sadece ihtiyaç duyulan alanlar istenir (code, name) — fazlası gelmez
    const query = `
      query {
        continents {
          code
          name
        }
      }
    `;

    // GraphQL'de her zaman POST kullanılır
    // body: { query: "..." } formatı — GraphQL standardı
    const response = await request.post(url, { data: { query } });

    // GraphQL hataları bile 200 döner! Hata → body.errors alanında gelir
    expect(response.status()).toBe(200);

    const responseBody = await response.json();

    // GraphQL yanıtı her zaman { data: { ... } } yapısındadır
    // arrayContaining: array içinde bu objelerin bulunduğunu kontrol eder
    // objectContaining: objenin en azından bu alanları içerdiğini kontrol eder
    expect(responseBody.data.continents).toEqual(
      expect.arrayContaining([
        expect.objectContaining({ code: 'EU', name: 'Europe' }),
        expect.objectContaining({ code: 'AS', name: 'Asia' }),
      ])
    );
  });

  // ── Test 2: Belirli bir dili sorgula ─────────────────────────
  test('should fetch language details for Spanish', async ({ request }) => {

    // GraphQL argument: language(code: "es") → filtre parametresi
    // REST'teki /language?code=es ya da /language/es karşılığı
    const query = `
      query {
        language(code: "es") {
          name
          native
        }
      }
    `;

    const response = await request.post(url, { data: { query } });
    expect(response.status()).toBe(200);

    const responseBody = await response.json();

    // data.language → query'de language(...) diye çağırdık, aynı isimle gelir
    expect(responseBody.data.language.name).toBe('Spanish');
    expect(responseBody.data.language.native).toBe('Español');
  });
});

8 — GraphQL Nested (İç İçe) Query

GraphQL'in en güçlü özelliği: tek istekte birden fazla ilişkili veriyi çekebilirsin. Aşağıdaki örnekte ülke bilgisi ile o ülkenin kıtası tek sorguda geliyor.

continents-nested query.test.js — country → continent iç içe sorgu JavaScript
import { test, expect } from '@playwright/test';

const url = 'https://countries.trevorblades.com/';

test.describe('GraphQL API Test — Nested Query', () => {

  test('should fetch nested data: Turkey and its continent', async ({ request }) => {

    // ── Nested Query Yapısı ──────────────────────────────────────
    // country(code: "TR") → TR kodu ile Türkiye'yi filtrele
    //   name     → ülke adı
    //   capital  → başkent
    //   continent { → ilişkili obje (JOIN gibi düşün)
    //     name   →   kıta adı
    //   }
    // REST'te bu için 2 ayrı istek gerekirdi:
    //   GET /countries/TR  ve  GET /continents/{id}
    // GraphQL'de tek istekte geliyor!
    const query = `
      query {
        country(code: "TR") {
          name
          capital
          continent {
            name
          }
        }
      }
    `;

    const response = await request.post(url, { data: { query: query } });

    // Status her zaman 200 — GraphQL hatalar için bile 200 döner
    expect(response.status()).toBe(200);

    const responseBody = await response.json();

    // expect.soft() → bu assertion başarısız olsa bile test devam eder
    // Tüm alanları tek seferde kontrol etmek için kullanışlı
    // Normal expect() → ilk hata yapılan assertion'da durur
    expect.soft(responseBody.data.country.name).toBe('Turkey');
    expect.soft(responseBody.data.country.capital).toBe('Ankara');

    // Nested objeye nokta notasyonuyla erişim: country → continent → name
    expect.soft(responseBody.data.country.continent.name).toBe('Asia');
  });
});

GraphQL'de Hata Kontrolü

GraphQL API'lerde HTTP status kodu her zaman 200'dür. Hataları body içindeki errors alanından kontrol etmek gerekir:

GraphQL hata kontrolü pattern'i JavaScript
test('GraphQL hata durumu kontrolü', async ({ request }) => {
  // Var olmayan bir alan sorguluyoruz — GraphQL hata üretmeli
  const query = `
    query {
      country(code: "TR") {
        varOlmayanAlan
      }
    }
  `;

  const response = await request.post(url, { data: { query } });

  // ⚠️ GraphQL hataları için HTTP status yine 200 döner!
  // REST'teki gibi 400/404 bekleme — hata body içinde gelir
  expect(response.status()).toBe(200);

  const body = await response.json();

  // Hata varsa body.errors array'i dolu gelir
  expect(body.errors).toBeDefined();
  expect(body.errors.length).toBeGreaterThan(0);

  // Başarılı yanıtta ise data dolu, errors tanımsız olmalı
  // expect(body.errors).toBeUndefined(); ← başarılı senaryoda bunu kullan
});

REST vs GraphQL — Playwright'ta Karşılaştırma

REST API GraphQL API
HTTP Metodu GET / POST / PUT / DELETE Her zaman POST
Endpoint Her kaynak için ayrı URL Tek URL (/graphql)
İstek body'si data: { field: value } data: { query: "..." }
Başarı status 200 / 201 / 204 Her zaman 200
Hata status 400 / 404 / 500 200 — hata body.errors'da
Yanıt yapısı body.field body.data.queryName.field
İlişkili veri Birden fazla istek Tek istekte nested query
Playwright assertion toBeOK() güvenli Sadece status() === 200
GraphQL'de toBeOK() kullanma! expect(response).toBeOK() status 200-299 için geçer, ama GraphQL'de hatalı query de 200 döner. Bu nedenle GraphQL testlerinde body.errors'ın undefined olduğunu ayrıca kontrol et.

Tag Sistemi ile Filtreleme

Projede beş farklı test etiketi kullanılıyor. Bu etiketlerle sadece belirli testleri çalıştırabilirsin:

@smoke @critical @regression @ui @network
npx playwright test --grep "@smoke"

Hızlı doğrulama: en temel testler

npx playwright test --grep "@critical"

Deploy öncesi kritik yolları test et

npx playwright test --grep "@smoke|@critical"

Smoke VEYA critical — OR operatörü

npx playwright test --grep-invert "@regression"

Regression dışındaki her şey

CI/CD Stratejisi PR açıldığında sadece @smoke çalıştır (hızlı). Main'e merge olduğunda @smoke|@critical|@regression çalıştır (kapsamlı).

🌀 Hurl API Testing

Hurl, HTTP isteklerini düz metin formatında yazarak API'leri test etmenize olanak tanıyan bir araçtır. Okunabilir sözdizimi ve güçlü assertion sistemiyle API testlerini kolaylaştırır.

Hurl Dosyası Anatomisi

Temel Hurl Yapısı — Her bölümün açıklaması Hurl
# ──────────────────────────────────────────────────────────
# Bir Hurl dosyası birden fazla HTTP "Entry" içerebilir.
# Her entry: İstek + Yanıt + Assertion bölümlerinden oluşur.
# ──────────────────────────────────────────────────────────

# ① HTTP Metodu + URL — Zorunlu, her entry'nin başında olmalı
GET https://jsonplaceholder.typicode.com/posts/1

# ② Beklenen HTTP Durum Kodu — "HTTP" kelimesi + status code
HTTP 200

# ③ [Asserts] — Yanıt üzerinde yapılacak doğrulamalar
# JSON body'deki değerleri kontrol eder
[Asserts]
jsonpath "$.id"     == 1
jsonpath "$.title"  isString
jsonpath "$.userId" == 1

# Boş satır → Bir sonraki entry başlıyor

# ④ POST isteği — Body ile birlikte
POST https://jsonplaceholder.typicode.com/posts

# Content-Type header → JSON göndereceğimizi söylüyoruz
Content-Type: application/json

# ⑤ [Options] — Bu entry için özel davranışlar
[Options]
retry: 3       # Başarısız olursa 3 kez tekrar dene

# ⑥ Request body — JSON formatında
{
  "title": "Yeni Post",
  "body": "İçerik buraya",
  "userId": 1
}

HTTP 201
[Asserts]
jsonpath "$.id"    isInteger
jsonpath "$.title" == "Yeni Post"

Assertion Türleri

Hurl, yanıt üzerinde farklı bölümleri kontrol eden zengin assertion seçenekleri sunar:

Assertion Türü Örnek Açıklama
jsonpath jsonpath "$.name" == "Alice" JSON body'deki belirli bir değeri kontrol eder
jsonpath isString jsonpath "$.email" isString Değerin string tipinde olduğunu doğrular
jsonpath isInteger jsonpath "$.id" isInteger Değerin integer tipinde olduğunu doğrular
jsonpath exists jsonpath "$.data" exists JSON path'in var olduğunu doğrular
jsonpath count jsonpath "$.items" count == 10 Array eleman sayısını kontrol eder
header header "Content-Type" contains "json" Response header değerini kontrol eder
duration duration < 500 Yanıt süresinin ms cinsinden üst sınırı
status status == 200 [Asserts] içinde status code kontrolü

Assertion Karşılaştırma Operatörleri

== "değer"

Tam eşitlik

!= "değer"

Eşitsizlik

contains "metin"

String içerip içermediği

startsWith "önek"

Belirtilen önek ile başlıyor mu

matches "regex"

Regex pattern eşleşmesi

< 500

Sayısal karşılaştırma (duration için)

posts.hurl — Tam CRUD Testi

tests/api/posts.hurl Hurl
# ── TEST 1: Tüm postları getir ──────────────────────────────
GET https://jsonplaceholder.typicode.com/posts
HTTP 200
[Asserts]
# Kök seviyede bir array olduğunu doğrula
jsonpath "$"          isCollection
# Array'in tam olarak 100 eleman içerdiğini doğrula
jsonpath "$"          count == 100
# Content-Type header'ının json içerdiğini doğrula
header "Content-Type" contains "application/json"

# ── TEST 2: Tek post getir ──────────────────────────────────
GET https://jsonplaceholder.typicode.com/posts/1
HTTP 200
[Asserts]
jsonpath "$.id"     == 1
jsonpath "$.userId" == 1
jsonpath "$.title"  isString
jsonpath "$.body"   isString
# Yanıt süresi 500ms'den az olmalı
duration           < 500

# ── TEST 3: Yeni post oluştur ───────────────────────────────
POST https://jsonplaceholder.typicode.com/posts
Content-Type: application/json
{
  "title": "Test Başlığı",
  "body": "Test içeriği",
  "userId": 1
}
# Başarılı oluşturma → 201 Created
HTTP 201
[Asserts]
# API yeni kaydın ID'sini döndürmeli
jsonpath "$.id"    isInteger
jsonpath "$.title" == "Test Başlığı"

# ── TEST 4: Post güncelle ───────────────────────────────────
PUT https://jsonplaceholder.typicode.com/posts/1
Content-Type: application/json
{
  "id": 1,
  "title": "Güncellenmiş Başlık",
  "body": "Güncellenmiş içerik",
  "userId": 1
}
HTTP 200
[Asserts]
jsonpath "$.title" == "Güncellenmiş Başlık"

# ── TEST 5: Post sil ────────────────────────────────────────
DELETE https://jsonplaceholder.typicode.com/posts/1
# Başarılı silme → 200 OK (bazı API'ler 204 No Content döner)
HTTP 200

auth.hurl — Header ve Auth Testleri

tests/api/auth.hurl Hurl
# ── Bearer Token ile İstek ──────────────────────────────────
GET https://jsonplaceholder.typicode.com/posts/1

# Authorization header → "Bearer <token>" formatı
Authorization: Bearer my-secret-token-123
Accept: application/json

HTTP 200
[Asserts]
jsonpath "$.id" == 1

# ── Özel Header'lar ─────────────────────────────────────────
GET https://jsonplaceholder.typicode.com/posts
Accept: application/json
X-Custom-Header: test-value
X-Request-ID: 12345

HTTP 200
[Asserts]
jsonpath "$" isCollection

# ── Query Parametreleri ─────────────────────────────────────
# URL'e doğrudan yaz: ?param=değer¶m2=değer2
GET https://jsonplaceholder.typicode.com/posts?userId=1&_limit=5

HTTP 200
[Asserts]
# _limit=5 parametresi → tam 5 sonuç dönmeli
jsonpath "$" count == 5
# Tüm sonuçlar userId=1'e ait olmalı
jsonpath "$[0].userId" == 1

users.hurl — 404 ve Edge Case Testleri

tests/api/users.hurl (kısmen) Hurl
# ── Normal durum: Kullanıcı getir ───────────────────────────
GET https://jsonplaceholder.typicode.com/users/1
HTTP 200
[Asserts]
jsonpath "$.id"   == 1
# Belirli bir isim bekliyorsak string karşılaştırması yaparız
jsonpath "$.name" == "Leanne Graham"
jsonpath "$.email" matches ".*@.*\\..*"   # Regex ile email formatı

# ── İlişkili kaynak: Kullanıcının postları ──────────────────
GET https://jsonplaceholder.typicode.com/users/1/posts
HTTP 200
[Asserts]
jsonpath "$"            isCollection
jsonpath "$[0].userId"  == 1   # Tüm postlar bu kullanıcıya ait mi?

# ── Hata durumu: Var olmayan kullanıcı ──────────────────────
GET https://jsonplaceholder.typicode.com/users/9999
# 9999 ID'li kullanıcı yok → 404 Not Found bekliyoruz
HTTP 404
Dikkat: Hurl'de entry sırası önemlidir Bir entry'de başarısız assertion varsa Hurl durur ve sonraki entry'lere geçmez (--continue-on-error flag ile override edilebilir).

[Captures] — Entry'ler Arası Değer Taşıma

Hurl'ün en güçlü özelliklerinden biri [Captures] bloğudur. Bir yanıttan değer yakalayıp sonraki entry'lerde {{değişken_adı}} söz dizimiyle kullanabilirsiniz. Bu sayede gerçekçi iş akışları yazılır: kayıt oluştur → ID'yi yakala → o ID ile güncelle.

[Captures] söz dizimi — kaynak türleri Hurl
GET https://api.example.com/items
HTTP 200

# [Captures]: Yanıttan değerleri değişkene atar
# Format: değişken_adı: kaynak "path"
[Captures]
item_id:    jsonpath  "$.items[0].id"      # JSON body'den
auth_token: header    "X-Auth-Token"        # Response header'dan
session:    cookie    "session_id"          # Cookie'den
redirect:   header    "Location"            # Redirect URL'den
sc:         status                          # HTTP status code'un kendisi

# Bir sonraki entry'de {{ }} ile kullan
GET https://api.example.com/items/{{item_id}}
Authorization: Bearer {{auth_token}}
HTTP 200
KaynakSöz DizimiNe Yakalar?
jsonpathid: jsonpath "$.data.id"JSON response body'den değer
headertoken: header "Authorization"Belirtilen response header değeri
cookiesid: cookie "session_id"Set-Cookie header'ından cookie değeri
xpathtitle: xpath "//h1/text()"XML/HTML body'den XPath ile
regexcode: regex "code=(\d+)"Body'de regex ile eşleşen ilk grup
statussc: statusHTTP status code'un kendisi

workflow_demo.hurl — Gerçek Dünya Senaryosu

Dört bağımlı adımı tek dosyada zincirler: Ürün Ara → İlk Ürünü Seç → Giriş Yap → Sepete Ekle. Her adım bir öncekinden yakaladığı değeri kullanır.

workflow_demo.hurl — Search → Detail → Auth → Cart zinciri Hurl
# Senaryo: Ürün Ara → İlk Ürünü Seç → Giriş Yap → Sepete Ekle

###############################################################
# 1. Ürün Arama
###############################################################

GET https://dummyjson.com/products/search?q=phone
HTTP 200

# Bu yanıttan iki değer yakalıyoruz — dosya boyunca {{ }} ile kullanılabilir
[Captures]
first_product_id:   jsonpath "$.products[0].id"
first_product_name: jsonpath "$.products[0].title"


###############################################################
# 2. Ürün Detay — yakalanan ID ile URL oluştur
###############################################################

GET https://dummyjson.com/products/{{first_product_id}}
HTTP 200
[Asserts]
# String'e gömünce tırnak içinde → "{{first_product_name}}"
# Sayıya gömünce tırnaksız         → {{first_product_id}}
jsonpath "$.title" == "{{first_product_name}}"
jsonpath "$.id"    == {{first_product_id}}


###############################################################
# 3. Giriş Yap — Token ve user_id yakala
###############################################################

POST https://dummyjson.com/auth/login
Content-Type: application/json
{
  "username": "emilys",
  "password": "emilyspass"
}
HTTP 200
[Captures]
# JWT token → bir sonraki istekte Authorization: Bearer {{temp_token}} olacak
temp_token: jsonpath "$.accessToken"
# Kullanıcı ID → sepet isteğindeki userId alanına girecek
user_id:    jsonpath "$.id"


###############################################################
# 4. Sepete Ekle — 3 farklı entry'den yakalanan değerleri birleştir
###############################################################

POST https://dummyjson.com/carts/add
Authorization: Bearer {{temp_token}}        # ← adım 3'ten
Content-Type: application/json
{
  "userId": {{user_id}},                    # ← adım 3'ten
  "products": [
    {
      "id": {{first_product_id}},             # ← adım 1'den
      "quantity": 2
    }
  ]
}
HTTP 201
[Asserts]
jsonpath "$.products[0].id"      == {{first_product_id}}
jsonpath "$.products[0].quantity" == 2
jsonpath "$.totalProducts"        >= 1
  • 1
    GET /search → [Captures] first_product_id, first_product_name

    Arama sonucundan ilk ürünün ID ve adını yakala. Bunlar dosya boyunca tüm entry'lerde kullanılabilir.

  • 2
    GET /products/{{first_product_id}} → Tutarlılık kontrolü

    Yakalanan ID URL'e gömülür. Detaydaki adın aramayla örtüşüp örtüşmediğini doğrula.

  • 3
    POST /auth/login → [Captures] temp_token, user_id

    Login yanıtından JWT token ve kullanıcı ID'sini yakala. Token bir sonraki istekte header olacak.

  • 4
    POST /carts/add — 3 ayrı entry'den gelen değer tek istekte

    temp_token + user_id (adım 3) ve first_product_id (adım 1) aynı request'te birleşiyor.

petstore.hurl — Postman'dan Hurl'e Dönüşüm

Bir Postman collection'ından Hurl'e nasıl geçileceğini gösteren örnek. Postman'ın global değişkenleri Hurl'de [Captures] ile, pre-request scriptleri ise doğrudan [Asserts] bloğuyla karşılanır.

petstore.hurl — POST → Capture → GET ile doğrulama zinciri Hurl
# Çalıştır:
# hurl --test --variable PetStoreURL=https://petstore.swagger.io petstore.hurl

###############################################################
# 1. Yeni evcil hayvan kaydı oluştur
###############################################################

POST https://petstore.swagger.io/v2/pet
Content-Type: application/json
{
  "id": 99,
  "category": { "id": 1, "name": "cats1" },
  "name": "doggie",
  "photoUrls": ["doggie"],
  "tags": [{ "id": 99, "name": "tag #1" }],
  "status": "available"
}
HTTP 200
[Asserts]
jsonpath "$.id" == 99

# Postman: pm.globals.set("petId", petId)
# Hurl   : [Captures] ile bir sonraki entry'e taşı
[Captures]
petId: jsonpath "$.id"


###############################################################
# 2. Kaydı GET ile tam doğrula — Postman'ın eql() karşılığı
###############################################################
# Postman: pm.expect(response).to.eql(pm.globals.get("expectedResults"))
# Hurl   : Her alan ayrı jsonpath satırı olarak yazılır → daha okunabilir

GET https://petstore.swagger.io/v2/pet/{{petId}}
HTTP 200
[Asserts]
jsonpath "$.id"             == 99
jsonpath "$.category.id"   == 1
jsonpath "$.category.name" == "cats1"
jsonpath "$.name"          == "doggie"
jsonpath "$.photoUrls[0]"  == "doggie"
jsonpath "$.tags[0].id"    == 99
jsonpath "$.tags[0].name"  == "tag #1"
jsonpath "$.status"        == "available"


###############################################################
# 3. Global değişken yerine capture edilen değerle doğrula
###############################################################
# Postman: pm.globals.get("petId") ile ID karşılaştırılır
# Hurl   : capture edilen {{petId}} doğrudan assertion'da kullanılır

GET https://petstore.swagger.io/v2/pet/{{petId}}
HTTP 200
[Asserts]
# Hem yakalanan değişken hem de string assertion bir arada
jsonpath "$.id"            == {{petId}}    # capture değişkeni
jsonpath "$.category.name" == "cats1"
jsonpath "$.name"          == "doggie"
jsonpath "$.status"        == "available"
jsonpath "$.tags[0].name"  == "tag #1"

Postman → Hurl Dönüşüm Tablosu

Postman KavramıHurl KarşılığıAçıklama
pm.globals.set("key", val)[Captures] bloğuDeğeri yakalar ve sonraki entry'lere taşır
pm.globals.get("key"){{key}}Yakalanan değişkeni kullanır
pm.environment.set()--variable key=val CLI flagDışarıdan değişken enjekte eder
Pre-request ScriptGerekmezHurl'de entry'ler zaten sıralı çalışır
pm.expect(res).to.eql(obj)Tek tek jsonpath satırlarıHer alan açıkça yazılır, daha okunabilir
Collection Variables--variables-file vars.envAyrı dosyadan toplu değişken yükler
--variable ile dışarıdan değer enjekte etmek hurl --test --variable PetStoreURL=https://petstore.swagger.io petstore.hurl — CLI'dan geçirilen değişkenler dosya içinde {{PetStoreURL}} söz dizimiyle kullanılır. CI/CD ortamında farklı base URL'ler için idealdir.
Dikkat: Hurl'de entry sırası önemlidir Bir entry'de başarısız assertion varsa Hurl durur ve sonraki entry'lere geçmez (--continue-on-error flag ile override edilebilir).

⚡ k6 Performance Testing

k6, geliştiriciler için tasarlanmış modern bir yük testi aracıdır. JavaScript ile test senaryoları yazar, binlerce sanal kullanıcı simüle ederek uygulamanın performansını ölçersiniz.

k6 Test Dosyası Anatomisi

k6 test dosyasının temel yapısı — tam açıklama JavaScript
// ① k6 modülleri — Node.js değil, k6 runtime'ı kullanır
import http from 'k6/http';       // HTTP istekleri
import { check, sleep } from 'k6'; // Assertions + bekleme

// ② export const options → k6'ya test davranışını bildirir
// Bu obje export edilmek ZORUNDA — k6 bunu otomatik okur
export const options = {

  // stages: Sanal kullanıcı sayısının zamanla nasıl değişeceğini tanımlar
  // "Ramp up" → yavaşça kullanıcı ekle, "Ramp down" → yavaşça azalt
  stages: [
    { duration: '10s', target: 5 },   // 0→5 VU: 10 saniyede 5 kullanıcıya ulaş
    { duration: '20s', target: 10 },  // 5→10 VU: 20 saniyede 10 kullanıcıya ulaş
    { duration: '10s', target: 0 },   // 10→0 VU: 10 saniyede sıfıra in (ramp down)
  ],

  // thresholds: Testin geçmesi için karşılanması gereken performans koşulları
  // Bu koşullardan biri sağlanmazsa test FAIL olarak işaretlenir
  thresholds: {
    // http_req_duration: İstek süresi metrikleri
    // p(95) → %95'lik yüzdelik dilim = isteklerin %95'i bu süreden az sürmeli
    'http_req_duration': ['p(95)<500'],

    // http_req_failed: Başarısız istek oranı (4xx/5xx yanıtlar)
    // rate<0.01 → hata oranı %1'den az olmalı
    'http_req_failed': ['rate<0.01'],
  },
};

// ③ default export function → Her sanal kullanıcının çalıştırdığı senaryo
// Bir iteration = bir VU'nun bu fonksiyonu bir kez çalıştırması
export default function () {

  // http.get() → GET isteği gönderir, response nesnesini döner
  const res = http.get('https://jsonplaceholder.typicode.com/posts/1');

  // check() → assertion yapar
  // İlk argüman: kontrol edilen değer
  // İkinci argüman: { 'test adı': (değer) => boolean } şeklinde kontroller
  check(res, {
    'status 200': (r) => r.status === 200,
    'body boş değil': (r) => r.body.length > 0,
    'yanıt <500ms': (r) => r.timings.duration < 500,
  });

  // sleep() → VU'nun bir sonraki isteği göndermeden önce beklemesi
  // Gerçek kullanıcı davranışını simüle eder (1 saniye bekleme)
  sleep(1);
}

Test Senaryoları

tests/performance/load-test.js — Normal Yük Testi JavaScript
import http from 'k6/http';
import { check, sleep } from 'k6';
import { generateSummary } from './summary.js';

export const handleSummary = generateSummary('stress-test');

export const options = {
  // LOAD TEST: Yavaşça yük ekle, kararlı yükte tut, yavaşça azalt
  // Amaç: Normal trafik altında sistemin davranışını ölçmek
  stages: [
    { duration: '30s', target: 10 }, // 30 saniyede 10 kullanıcıya çık
    { duration: '1m',  target: 10 }, // 1 dakika boyunca stabil devam et
    { duration: '30s', target: 0  }, // Yavaşça kapat
  ],
  thresholds: {
    http_req_duration: ['p(95)<500'], // İsteklerin %95'i 500ms altında olmalı
    http_req_failed:   ['rate<0.01'],  // Hata oranı %1'den az olmalı
  },
};

export default function () {
  // Gerçekçi Header Tanımı
  const params = {
    headers: {
      'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
      'Accept': 'application/json',
    },
  };

  // Güvenli test adresi: httpbin.test.k6.io/get
  // Bu adres header'ları JSON olarak geri döner.
  let res = http.get('https://httpbin.test.k6.io/get', params);

  // Kontroller
  check(res, {
    'bağlantı başarılı (200)': (r) => r.status === 200,
    'sunucu yanıt verdi mi?':   (r) => r.body.length > 0,
  });

  // İnsan davranışını taklit etmek için rastgele bekleme
  sleep(Math.random() * 2 + 1);
}
tests/performance/stress-test.js — Stres Testi JavaScript
export const options = {
  // STRESS TEST: Limitin üstüne çıkarak sistemin kırılma noktasını bul
  // Amaç: Kapasite limitlerini ve sistemin aşırı yük altındaki davranışını test etmek
  stages: [
    { duration: '30s', target: 10 }, // Normal yük
    { duration: '30s', target: 25 }, // Yükü artır
    { duration: '30s', target: 50 }, // Pik nokta: 50 VU
    { duration: '30s', target: 25 }, // Geri in
    { duration: '30s', target: 0  }, // Ramp down
  ],
  thresholds: {
    // Stres testinde P99 kullanılır (en kötü %1'i hariç tut)
    'http_req_duration': ['p(99)<1000'], // P99 < 1 saniye
    'http_req_failed': ['rate<0.05'],    // Hata oranı < %5 (daha toleranslı)
  },
};

export default function () {
  // http.batch() → Birden fazla isteği eşzamanlı gönderir (paralel)
  const responses = http.batch([
    ['GET', 'https://jsonplaceholder.typicode.com/posts'],
    ['GET', 'https://jsonplaceholder.typicode.com/users'],
    ['GET', 'https://jsonplaceholder.typicode.com/todos'],
  ]);

  responses.forEach((res) => {
    check(res, { 'status ok': (r) => r.status === 200 });
  });

  sleep(0.5); // Stres testinde daha kısa bekleme
}
tests/performance/spike-test.js — Spike Testi JavaScript
export const options = {
  // SPIKE TEST: Anlık büyük trafik artışını simüle eder
  // Amaç: Flash sale, viral post gibi ani yük artışlarını test etmek
  stages: [
    { duration: '30s', target: 2   }, // Normal: düşük trafik
    { duration: '2s',  target: 100  }, // SPIKE: 2 saniyede 100 VU!
    { duration: '10s', target: 100  }, // Spike'ı kısa süre tut
    { duration: '2s',  target: 2    }, // Normale dön
    { duration: '30s', target: 0    }, // Ramp down
  ],
  thresholds: {
    // Spike testinde en toleranslı eşikler kullanılır
    'http_req_duration': ['p(95)<2000'], // P95 < 2 saniye
    'http_req_failed': ['rate<0.10'],    // Hata oranı < %10
  },
};
Test Türü Amaç Max VU P Dilimi Hata Toleransı
Load Test Normal trafik performansı 10 p(95) < 500ms < %1
Stress Test Kapasite ve kırılma noktası 50 p(99) < 1000ms < %5
Spike Test Ani trafik patlaması 100 p(95) < 2000ms < %10

Stages — Yük Profili

stages dizisi, testin süresince sanal kullanıcı (VU) sayısının zamanla nasıl değişeceğini tanımlar. Her eleman iki alan içerir:

AlanTipAçıklama
durationstringBu aşamanın ne kadar süreceği — '30s', '1m', '2m30s' gibi
targetnumberAşama sonunda ulaşılacak VU sayısı. k6, önceki VU değerinden bu değere doğrusal olarak geçiş yapar
Stages — Yorum Satırlı Tam Örnek JavaScript
export const options = {
  stages: [

    // ── Ramp-up ──────────────────────────────────────────────────
    // k6 bu sürede VU sayısını 0'dan hedef değere doğrusal artırır.
    // Gerçek trafiği taklit etmek ve sunucuyu kademeli ısıtmak için kullanılır.
    { duration: '30s', target: 10 },

    // ── Steady-state (kararlı yük) ───────────────────────────────
    // VU sayısı sabit kalır. Sistemin sürdürülebilir performansı burada ölçülür.
    // target bir önceki aşamanın bitiş değeriyle aynıysa VU değişmez.
    { duration: '1m',  target: 10 },

    // ── Ramp-down ────────────────────────────────────────────────
    // VU sayısını sıfıra indirerek testi temiz şekilde sonlandırır.
    // target: 0 → tüm VU'lar serbest bırakılır.
    { duration: '30s', target: 0  },

  ],
};

Zaman Çizelgesi

Ramp-up
Steady-state
Ramp-down
0 → 10 VU
30s
10 VU sabit
1m
10 → 0 VU
30s
target değeri nasıl yorumlanır? k6 her aşamada, bir önceki aşamanın son VU değerinden bu aşamanın targetına doğru düzgün şekilde geçiş yapar. Örneğin önceki aşama target: 10 ile bitiyorsa ve yeni aşama target: 50 ise, k6 duration süresince 10'dan 50'ye çıkar. target: 0 koyarak aşamayı sona erdirebilirsiniz.

Threshold ve Metrikler

k6 Threshold Yazım Formatları JavaScript
export const options = {
  thresholds: {

    // ── HTTP İstek Süresi ──────────────────────────────────────
    'http_req_duration': [
      'avg<200',      // Ortalama yanıt süresi 200ms'den az olmalı
      'p(50)<150',    // Medyan (P50) 150ms'den az olmalı
      'p(90)<300',    // Yüzde 90'ı 300ms'den az olmalı
      'p(95)<500',    // Yüzde 95'i 500ms'den az olmalı
      'p(99)<1000',   // Yüzde 99'u 1 saniyeden az olmalı
      'max<2000',     // En uzun istek bile 2 saniyeden az olmalı
    ],

    // ── Hata Oranı ────────────────────────────────────────────
    'http_req_failed': [
      'rate<0.01',    // Hata oranı %1'den az
    ],

    // ── İstek Sayısı ──────────────────────────────────────────
    'http_reqs': [
      'count>1000',   // Test boyunca en az 1000 istek yapılmış olmalı
      'rate>100',     // Saniyede en az 100 istek (throughput)
    ],

    // ── Özel Metrik Threshold'u ───────────────────────────────
    // Sadece belirli bir grup için threshold tanımla
    'http_req_duration{endpoint:posts}': ['p(95)<300'],
  },
};

Önemli k6 Metrikleri

Metrik Ne ölçer? Birim
http_req_duration İstek başlangıcından yanıt alınmasına kadar geçen süre ms
http_req_failed HTTP 4xx/5xx veya bağlantı hatalarının oranı rate (0–1)
http_reqs Toplam ve saniyedeki istek sayısı count / rate
vus Anlık aktif sanal kullanıcı sayısı count
vus_max Test boyunca ulaşılan maksimum VU sayısı count
http_req_connecting TCP bağlantı kurma süresi ms
http_req_tls_handshaking TLS el sıkışma süresi ms
http_req_sending Request body gönderme süresi ms
http_req_receiving Response body alma süresi ms
Yüzdelik Dilim (Percentile) Nedir? p(95) < 500ms → "Tüm isteklerin %95'i 500ms'den kısa sürdü" anlamına gelir. Kalan %5 bu eşiği aşabilir. P99 daha katı, P50 (medyan) daha toleranslıdır. Gerçek dünya SLA'ları genellikle P95 veya P99 üzerine kurulur.

⚙️ GitHub Actions CI/CD

Proje testleri GitHub Actions ile otomatik olarak çalıştırılabilir. Workflow tamamen manual tetikleme (workflow_dispatch) üzerine kurulu, 3 paralel job içeriyor.

Workflow Girdi Parametreleri

Actions → Tests sekmesinden "Run workflow" butonuna tıkladığında aşağıdaki parametreleri girebilirsin:

Parametre Varsayılan Seçenekler Açıklama
test_suite all all / web / api / performance Hangi test grubunun çalışacağı
playwright_tag @smoke @smoke, @regression, @ui, @network, @critical Playwright tag filtresi
playwright_grep_invert @regression vb. Bu tag dışındaki testleri çalıştır
playwright_project chromium chromium / firefox / mobile-safari Kullanılacak tarayıcı
playwright_file tests/web/homepage.spec.ts vb. Belirli spec dosyasını çalıştır
hurl_glob tests/api/**/*.hurl Hangi Hurl dosyalarının çalışacağı
hurl_verbose false true / false Detaylı HTTP log çıktısı
k6_test all load-test / stress-test / spike-test / all Hangi k6 testinin çalışacağı
k6_vus Sayı VU sayısını override et
k6_duration 30s, 1m vb. Süreyi override et

Job Yapısı

.github/workflows/tests.yml — 3 Paralel Job YAML
name: Run Tests
on:
  # Sadece manuel tetikleme — otomatik push/PR trigger yok
  workflow_dispatch:
    inputs:
      test_suite:
        type: choice
        options: [all, web, api, performance]

jobs:

  # ── JOB 1: Playwright Web Testleri ──────────────────────────
  web-tests:
    if: ${{ inputs.test_suite == 'all' || inputs.test_suite == 'web' }}
    runs-on: ubuntu-latest
    # Playwright'ın tüm tarayıcılarla hazır geldiği resmi container
    container:
      image: mcr.microsoft.com/playwright:v1.58.2-jammy
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - name: Run Playwright Tests
        run: |
          npx playwright test \
            --grep "${{ inputs.playwright_tag }}" \
            --project="${{ inputs.playwright_project }}"
      # Raporları artifact olarak sakla (30 gün)
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: playwright-report
          path: reports/playwright/
          retention-days: 30

  # ── JOB 2: Hurl API Testleri ────────────────────────────────
  api-tests:
    if: ${{ inputs.test_suite == 'all' || inputs.test_suite == 'api' }}
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      # Hurl binary'sini indir ve kur
      - name: Install Hurl
        run: |
          curl -LO https://github.com/Orange-OpenSource/hurl/releases/download/7.1.0/hurl_7.1.0_amd64.deb
          sudo dpkg -i hurl_7.1.0_amd64.deb
      - name: Run Hurl Tests
        run: hurl --test --glob "${{ inputs.hurl_glob }}"

  # ── JOB 3: k6 Performans Testleri ───────────────────────────
  performance-tests:
    if: ${{ inputs.test_suite == 'all' || inputs.test_suite == 'performance' }}
    runs-on: ubuntu-latest
    # Grafana'nın resmi k6 container image'ı
    container:
      image: grafana/k6:latest
    steps:
      - uses: actions/checkout@v4
      - name: Run k6 Load Test
        run: k6 run tests/performance/load-test.js
Artifact'lar Her job tamamlandıktan sonra test raporları GitHub'a yüklenir. Actions sayfasında ilgili workflow run'a tıklayarak "Artifacts" bölümünden HTML raporları indirebilirsin (30 gün boyunca saklanır).

Önerilen CI Stratejisi

PR Açıldığında

test_suite: web · tag: @smoke · Hızlı doğrulama, 2-3 dakika

Main'e Merge Sonrası

test_suite: all · tag: @smoke|@critical · Tam doğrulama

Haftalık / Release Öncesi

test_suite: all · tag: @regression · k6 ile yük testleri dahil

Spesifik Dosya Debug

playwright_file: tests/web/pom-api.spec.ts · Tek dosya çalıştır