Soru “AI test yazar mı” değil, “hangi adımı devralır”
AI araçlarını değerlendiren QA lead’lerin çoğu yanlış soruyu soruyor: “Bu araç testlerimizi yazabilir mi?” Bu soru bütün bir süreci tek bir kutuya sıkıştırdığı için cevabı da işe yaramaz. Test otomasyonu tek bir iş değil; birbirinden farklı maliyet ve risk profilleri olan yedi ayrı adım.
Ayıralım: keşif (uygulamanın mevcut hâlini anlamak), senaryo yazımı (neyi doğrulayacağımıza karar vermek), locator seçimi, kod üretimi, review, bakım, hata ayıklama. LLM ajanı bugün keşif, locator seçimi, kod üretimi ve hata ayıklama hipotezinde gerçekten kazandırıyor. Senaryo yazımında — yani hangi iş kuralının doğrulanacağına karar vermekte — kazandırmıyor, çünkü bu bilgi uygulamada değil, ürün kararında duruyor. Review’da ise doğrudan maliyet yaratıyor: üretilmiş kodun gözden geçirilmesi elle yazılmış kodun gözden geçirilmesinden daha yavaştır, çünkü yazan kişinin kafasındaki gerekçeyi okuyamazsınız.
Karar bu dengeden çıkar: ajanın kazandırdığı dakika sayısı, yarattığı review yükünden büyükse değerlidir. Aksi hâlde değil. Yazının geri kalanı bu dengeyi ölçülebilir hâle getirmekle ilgili.
LLM ajanının test akışındaki yeri
Ajan, test sürecinin yerine geçen bir sistem değil; zincirin bir halkası. Akış tek yönlü değil, bir kapısı var.

Kritik nokta şu: ajanın çıktısı doğrudan ana dala gitmez. Review kapısı opsiyonel bir nezaket değil, mimarinin parçası. Sebebi teknik: LLM çağrısı olasılıksaldır, aynı istem iki farklı test üretebilir. Ama üretilen Playwright testi deterministiktir — dosyaya yazıldığı andan itibaren her koşuda aynı şeyi yapar.
Yani determinizm ajanın özelliği değil, üretilen kodun özelliğidir. Bu ayrım bütün CI politikasını belirler: ajanı üretim zamanında (geliştirici makinesi, PR) tutar, çalışma zamanında (CI koşusu) tutmazsınız.
Playwright MCP: ajana sayfanın gerçek halini vermek
Bir LLM’e ekran görüntüsü verip “bu butona tıkla” demek, onu piksel tahminine zorlar. Üretilen locator da tahmin olur: div.container > div:nth-child(3) > button. İlk CSS değişikliğinde kırılır.
Playwright MCP sunucusu bu problemi başka bir veri kaynağıyla çözer. Ajana görüntü değil, Playwright’ın tarayıcıdan aldığı erişilebilirlik ağacı anlık görüntüsü verilir. Bu ağaçta her öğenin ARIA rolü, erişilebilir ismi ve durumu yazar. Ajan “ekranda sağ üstte mavi bir şey var” diye tahmin etmez; button "Siparişi Onayla" diye okur. Ürettiği locator da doğal olarak getByRole('button', { name: 'Siparişi Onayla' }) olur — yani DOM yapısına değil, kullanıcının gördüğü semantiğe bağlı bir seçici.
Kurulum, MCP destekleyen istemcinin (Claude Code, Cursor, VS Code Copilot) yapılandırma dosyasına bir sunucu eklemekten ibaret:
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": [
"@playwright/mcp@latest",
"--browser", "chromium",
"--isolated"
]
}
}
}
--isolated, her oturumun temiz bir profille başlamasını sağlar; önceki oturumdan kalan çerez ve oturum durumu ajanı yanıltmaz.
Bir oturum pratikte şöyle ilerler. Ajana verilen istem:
http://localhost:3000/loginadresine git.de**@***ek.com / Parola123!ile giriş yap, ardından ürün arama kutusuna “klavye” yaz ve ilk sonucu sepete ekle. Her adımda anlık görüntü al. Sonundatests/sepet-ekleme.spec.tsdosyasını oluştur.
Ajan sırayla browser_navigate, browser_snapshot, browser_type, browser_click araçlarını çağırır. Aldığı anlık görüntü şuna benzer:
- document:
- heading "Giriş Yap" [level=1]
- textbox "E-posta"
- textbox "Parola"
- button "Giriş Yap"
- link "Parolamı unuttum"
Bu ağaçtan üretilen test:
import { test, expect } from '@playwright/test';
test('giriş yapan kullanıcı ürünü sepete ekleyebilir', async ({ page }) => {
await page.goto('http://localhost:3000/login');
await page.getByRole('textbox', { name: 'E-posta' }).fill('de**@***ek.com');
await page.getByRole('textbox', { name: 'Parola' }).fill('Parola123!');
await page.getByRole('button', { name: 'Giriş Yap' }).click();
await expect(page.getByRole('heading', { name: 'Ürünler' })).toBeVisible();
await page.getByRole('searchbox', { name: 'Ürün ara' }).fill('klavye');
await page.getByRole('button', { name: 'Ara' }).click();
const ilkSonuc = page.getByRole('listitem').filter({ hasText: 'klavye' }).first();
await ilkSonuc.getByRole('button', { name: 'Sepete Ekle' }).click();
await expect(page.getByRole('status', { name: 'Sepet' })).toHaveText('1 ürün');
});
Locator kalitesi buradaki tek gerçek kazanç ve küçümsenecek bir kazanç değil — kırılgan seçici, flaky testin en yaygın kaynaklarından biridir. Playwright’ın bu konudaki konumu için araç karşılaştırmasına bakabilirsiniz.
Test üretimi: ajan iyi olduğu yer, kötü olduğu yer
Ajan üç işte hızlı: boş dosyadan çalışır iskelet çıkarmak, tekrarlayan CRUD akışlarını yazmak, mevcut bir testi başka bir veri setine uyarlamak. Bu üçü de “yapıyı kopyala” işidir ve LLM’ler yapı kopyalamakta iyidir.
Üç işte ise tipik olarak kötü: iş kuralı doğrulamasını uydurur (uygulamada ne görürse onu doğru kabul eder — yani bug’ı spesifikasyon sanır), assertion’ları zayıf tutar (öğenin varlığını kontrol eder, değerini kontrol etmez) ve emin olamadığı yere sabit bekleme ekler.
Aynı senaryonun zayıf üretilmiş hâli:
// ZAYIF
test('indirim uygulanır', async ({ page }) => {
await page.goto('/sepet');
await page.waitForTimeout(3000);
await page.fill('#kupon', 'YAZ20');
await page.click('.btn-primary');
await page.waitForTimeout(2000);
await expect(page.locator('.toplam')).toBeVisible();
});
Bu test neredeyse hiçbir şey doğrulamıyor. Toplam tutar yanlış hesaplansa, indirim hiç uygulanmasa, kupon geçersiz sayılsa bile yeşil kalır. Tek garantisi .toplam sınıfına sahip bir elementin ekranda olması.
Aynı senaryonun doğrulanabilir hâli:
// SAĞLAM
test('YAZ20 kuponu ara toplamdan %20 indirir', async ({ page }) => {
await page.goto('/sepet');
await expect(page.getByTestId('ara-toplam')).toHaveText('1.000,00 TL');
await page.getByRole('textbox', { name: 'Kupon kodu' }).fill('YAZ20');
await page.getByRole('button', { name: 'Kuponu Uygula' }).click();
await expect(page.getByRole('status')).toHaveText('Kupon uygulandı: YAZ20');
await expect(page.getByTestId('indirim-tutari')).toHaveText('-200,00 TL');
await expect(page.getByTestId('genel-toplam')).toHaveText('800,00 TL');
});
Fark prompt kalitesi değil, gereksinim kalitesi. İkinci testi ajan ancak beklenen tutarları biliyorsa yazabilir. Bilmiyorsa birincisini yazar — ve bu, zayıf gereksinimin koda yansımasıdır.
Kabul kriterinden teste: ajana ne verdiğin çıktıyı belirliyor
Belirsiz user story veren ekip belirsiz test alır. “Kullanıcı kupon kullanabilmeli” cümlesinden yukarıdaki sağlam test çıkmaz; çıkamaz, çünkü beklenen değer o cümlede yok.
Ajana verilecek asgari paket üç parçadan oluşur: Given/When/Then formatında kabul kriteri, veri ön koşulları, beklenen hata mesajları.
Senaryo: Süresi dolmuş kupon reddedilir
Diyelim ki "test-kullanici" hesabı var ve sepetinde 1.000,00 TL tutarında ürün var
Ve "BAHAR10" kuponunun son geçerlilik tarihi dünden önce
Eğer kullanıcı kupon kutusuna "BAHAR10" yazıp "Kuponu Uygula" butonuna basarsa
O zaman "Bu kuponun geçerlilik süresi dolmuş." hata mesajı görünür
Ve genel toplam "1.000,00 TL" olarak kalır
Ve kupon kutusu boşaltılmaz
Bu girdiyle üretilen test, veri ön koşulunu da kendi kurar:
import { test, expect } from '@playwright/test';
import { apiIleSepetHazirla, suresiDolmusKuponOlustur } from './yardimcilar/veri';
test('süresi dolmuş kupon reddedilir ve toplam değişmez', async ({ page, request }) => {
const kullanici = await apiIleSepetHazirla(request, { tutar: 1000 });
const kupon = await suresiDolmusKuponOlustur(request, { kod: 'BAHAR10' });
await page.goto(`/sepet?oturum=${kullanici.oturumAnahtari}`);
await expect(page.getByTestId('genel-toplam')).toHaveText('1.000,00 TL');
await page.getByRole('textbox', { name: 'Kupon kodu' }).fill(kupon.kod);
await page.getByRole('button', { name: 'Kuponu Uygula' }).click();
await expect(page.getByRole('alert'))
.toHaveText('Bu kuponun geçerlilik süresi dolmuş.');
await expect(page.getByTestId('genel-toplam')).toHaveText('1.000,00 TL');
await expect(page.getByRole('textbox', { name: 'Kupon kodu' })).toHaveValue('BAHAR10');
});
Bu bir prompt mühendisliği başarısı değil; kabul kriterinin disiplinli yazılmış olmasının sonucu. Ajanı ekibe sokmanın en gerçekçi yan etkisi de bu: belirsiz gereksinimler artık görünür hâle gelir, çünkü belirsiz gereksinim belirsiz test üretir ve bunu review’da herkes görür.
Üretilen testi review ederken bakılacak altı şey
Ajan üretimi testlerde tekrar eden hata örüntüleri bellidir; review listesi de bu örüntülere göre kurulur. Her madde için reddetme eşiği net olmalı — “biraz zayıf ama geçsin” diyen ekip birkaç ay sonra bakımını yapamadığı bir test setiyle kalır.
- Assertion iş sonucunu doğruluyor mu? Yalnızca
toBeVisible()içeren test reddedilir. En az bir assertion somut bir değer, tutar veya metin karşılaştırmalı olmalı. - Locator semantik mi?
getByRole,getByLabel,getByTestIdkabul.nth-child, derin CSS zinciri, konuma bağlı XPath reddedilir. - Test kendi verisini üretiyor mu? Ortamda hazır bulunan bir kayda isimle bağlanan test reddedilir. Gerekçesi ayrı bir yazının konusu: her testin kendi verisini üretmesi neden zorunlu.
- Sabit bekleme var mı?
waitForTimeoutiçeren test istisnasız reddedilir. Yerine web-first assertion veyawaitForResponsegelir. - Başka testin durumuna bağımlı mı? Test tek başına, rastgele sırada ve paralel koşabilmeli.
test.describe.serialkullanımı gerekçe ister. - Başarısız olduğunda sebebi söylüyor mu? “Timeout 30000ms exceeded” diyen bir hata işe yaramaz. Assertion’lar, kırıldığında hangi iş kuralının bozulduğunu söyleyecek şekilde adlandırılmış olmalı.
Bu altı maddeden ilk ikisi ve dördüncüsü, üretilmiş testlerde en sık düzeltme gerektiren yerlerdir; çünkü modelin yapabileceği en güvenli hamle “zayıf ama yeşil” testtir.
Self-healing locator neden sorunu çözmüyor
Satıcı sunumlarının vazgeçilmez slaytı: “Locator kırıldığında araç kendi kendini onarır.” Teknik olarak doğru, test stratejisi olarak zararlı.
Self-healing, kırık bir locator’ı yeniden çalışır hâle getirir. Testin doğru şeyi doğruladığını garanti etmez. Düşünün: geliştirici “Siparişi Onayla” butonunu yanlışlıkla devre dışı bıraktı ve yerine görsel olarak benzer başka bir buton geldi. Semantik locator bunu yakalar, test kırmızı yanar, regresyon görünür olur. Self-healing mekanizma ise en yakın benzer öğeyi bulur, ona tıklar, test yeşil kalır — ve gerçek bir regresyon sessizce üretime gider.
Yani self-healing, kırılganlığı çözmez; kırılganlığı sinyal olmaktan çıkarır. Bir test paketinin değeri yeşil kalma oranından değil, kırıldığında doğru şeyi söylemesinden gelir.
Doğru yaklaşım iki adımlı: locator’ı en baştan semantik yapmak (MCP’nin erişilebilirlik ağacı üzerinden çalışmasının asıl değeri burada) ve UI değiştiğinde locator güncellemesini otomatik değil, bilinçli bir review kararı olarak ele almak. Değişiklik kasıtlıysa locator güncellenir; kasıtsızsa bug kaydı açılır. Bu ayrımı bir algoritma sizin yerinize yapamaz, çünkü “kasıt” kodda yazmıyor.
Bakım ve hata ayıklama: asıl kazancın olduğu yer
LLM’in test sürecindeki en gerçekçi getirisi kod üretmek değil, okumak. Bir flaky testin kök nedenini bulmak için üç farklı veri kaynağını aynı anda zihinde tutmanız gerekir: Playwright trace’i (adım adım zaman çizelgesi), hata yığını ve başarısızlık anındaki DOM anlık görüntüsü. İnsan bu üçünü sırayla okur; model aynı anda okur.
Pratikte akış şudur: başarısız koşunun trace’ini, ilgili test dosyasını ve hata çıktısını ajana verirsiniz, karşılığında sıralı kök neden hipotezleri istersiniz. Tipik çıktı şöyle görünür: “Tıklama 1.240 ms’de gerçekleşti, ancak /api/sepet yanıtı 1.390 ms’de döndü; buton tıklama anında henüz bağlanmamış bir handler’a sahip olabilir.” Bu bir cevap değil, hipotezdir. Doğrulamayı insan yapar — handler bağlanma anını logla, testi yüz kez koştur, hipotezi ya onayla ya çürüt.
Bu akışın kod üretiminden daha düşük riskli olmasının sebebi basit: ajanın çıktısı repoya girmiyor. Hipotez yanlışsa kaybedilen şey birkaç dakika; üretilen test yanlışsa kaybedilen şey aylarca yanlış güven. Teşhis sürecinin tamamı için flaky testleri teşhis etmek yazısına bakın; ajan bu sürecin yerine geçmez, ilk hipotez adımını kısaltır.
CI’da AI üretimi testleri çalıştırmanın kuralları
İki kural yeterli.
Birincisi: ajan CI içinde canlı çalışmaz. Yani pipeline içinde, koşma anında locator uyduran bir mekanizma kullanılmaz. Üç gerekçe: maliyet (her koşuda token faturası), determinizm (aynı commit iki farklı sonuç verir, bu artık test değildir) ve denetlenebilirlik (bir ay sonra “bu test o gün neyi doğruladı” sorusunun cevabı yoktur). Üretim aşaması geliştirici makinesinde ve PR içinde kalır; CI yalnızca repoya yazılmış, review’dan geçmiş deterministik kodu koşar.
İkincisi: üretilen testler karantinada başlar. Ayrı bir etiketle işaretlenir, ana setin sonucunu etkilemeden koşar, belirli sayıda ardışık yeşil koşudan sonra etiketi kaldırılır.
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './tests',
retries: 0,
reporter: [['list'], ['html', { open: 'never' }]],
projects: [
{
name: 'ana-set',
grepInvert: /@ai-karantina/,
use: { baseURL: process.env.BASE_URL ?? 'http://localhost:3000' },
},
{
name: 'karantina',
grep: /@ai-karantina/,
use: { baseURL: process.env.BASE_URL ?? 'http://localhost:3000' },
},
],
});
# .github/workflows/e2e.yml
name: E2E
on: [pull_request]
jobs:
ana-set:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20', cache: 'npm' }
- run: npm ci
- run: npx playwright install --with-deps chromium
- run: npx playwright test --project=ana-set
karantina:
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20', cache: 'npm' }
- run: npm ci
- run: npx playwright install --with-deps chromium
- run: npx playwright test --project=karantina --repeat-each=3
- uses: actions/upload-artifact@v4
if: always()
with:
name: karantina-raporu
path: playwright-report/
--repeat-each=3 karantina setinde kararsızlığı erken açığa çıkarır: üç tekrarın üçü de yeşil değilse test ana sete terfi etmez. continue-on-error: true ise karantinanın PR’ı bloklamamasını sağlar — yeni testlerin ilk günlerinde bu şart, yoksa ekip karantinayı tamamen devre dışı bırakır.
Pilot kurmak: iki haftada ölçülebilir bir deneme
Araç seçmeden önce ölçüm kurun. Satıcı demosu size hiçbir şey söylemez, çünkü demo uygulaması sizin uygulamanız değil.

Birkaç ayrıntı planı işe yarar kılar. Senaryoları ikiye bölerken zorluk dengesini koruyun — basit CRUD akışlarını ajana, karmaşık iş kuralı senaryolarını insana verirseniz sonuç baştan belli olur. Aynı review listesini (yukarıdaki altı madde) her iki gruba da uygulayın; ajan çıktısına farklı standart uygulamak ölçümü bozar. Review süresini PR açılışından onaya kadar değil, fiilen harcanan dakika olarak kaydedin.
Karar kriteri tek bir cümle: review + bakım toplam maliyeti, elle yazma maliyetinden düşük mü? “Ajan beş dakikada yazdı” bir sonuç değil; o testin ilk ayında kaç kez düzeltildiği sonuçtur.
İki hafta sonunda cevap “hayır” çıkabilir — bu da geçerli bir sonuçtur ve karar vermek için elinizde veri olur. “Evet” çıkarsa kapsamı genişletmeden önce karantina politikasını ve review listesini yazılı hâle getirin; ölçtüğünüz kazanç, disiplin gevşediği anda kaybolur. Konunun daha geniş çerçevesi için yapay zeka destekli test otomasyonu yazısına, test ortamını ajandan bağımsız hâle getirmek için de mocking ve stubbing yazısına bakabilirsiniz.