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.
Proje Yapısı
Projedeki dosyaların organizasyonu ve her klasörün amacı.
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.
| Araç | Runtime | Kurulum | package.json | Dosya 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) |
.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.
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:
// ① 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
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
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();
});
});
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.
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();
}
}
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();
}
}
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/);
});
});
-
1BasePage — Ortak Davranışlar
navigate(), getTitle(), waitForNetworkIdle() gibi tüm sayfalarda kullanılacak metodlar buraya taşınır.
-
2Sayfa Sınıfı — Locator'lar + Metodlar
Her sayfanın elementleri readonly Locator olarak, etkileşimleri metod olarak tanımlanır.
-
3Spec Dosyası — Sadece Test Mantığı
Testler POM metodlarını çağırır; selector'lar veya implementation detayları içermez.
Network Intercept ve API Mock
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() → 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.
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
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.
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() };
}
}
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.
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.
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?
// ── 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
});
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.
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.');
});
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.
/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 } }).
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.
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:
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 |
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.
🌀 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
# ──────────────────────────────────────────────────────────
# 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
# ── 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
# ── 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
# ── 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
[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.
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
| Kaynak | Söz Dizimi | Ne Yakalar? |
|---|---|---|
jsonpath | id: jsonpath "$.data.id" | JSON response body'den değer |
header | token: header "Authorization" | Belirtilen response header değeri |
cookie | sid: cookie "session_id" | Set-Cookie header'ından cookie değeri |
xpath | title: xpath "//h1/text()" | XML/HTML body'den XPath ile |
regex | code: regex "code=(\d+)" | Body'de regex ile eşleşen ilk grup |
status | sc: status | HTTP 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.
# 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
-
1GET /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.
-
2GET /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.
-
3POST /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.
-
4POST /carts/add — 3 ayrı entry'den gelen değer tek istekte
temp_token+user_id(adım 3) vefirst_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.
# Ç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ğu | Değ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 flag | Dışarıdan değişken enjekte eder |
| Pre-request Script | Gerekmez | Hurl'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.env | Ayrı dosyadan toplu değişken yükler |
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.
⚡ 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 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ı
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);
}
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
}
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:
| Alan | Tip | Açıklama |
|---|---|---|
duration | string | Bu aşamanın ne kadar süreceği — '30s', '1m', '2m30s' gibi |
target | number | Aşama sonunda ulaşılacak VU sayısı. k6, önceki VU değerinden bu değere doğrusal olarak geçiş yapar |
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
30s1m30stargetı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
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 |
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ı
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
Ö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