İçeriğe geç

Page Object deseni ne zaman işe yarar, ne zaman yük olur

Page Object neden sorgulanmadan uygulanıyor

Yeni bir Playwright projesine başlayan ekiplerin çoğu ilk gün pages/ klasörünü açıyor. Neden açtığını soran yok; “test otomasyonu böyle yapılır” diye biliniyor. Oysa Page Object deseni belirli bir teknik gerçekliğin çözümü olarak doğdu: 2010’ların Selenium WebDriver dünyası. O dünyada elinizde kırılgan XPath ifadeleri vardı, her etkileşimden önce WebDriverWait yazmanız gerekiyordu, StaleElementReferenceException günlük rutindi ve Java/C# gibi derlenen dillerde sınıf yazmak zaten varsayılan organizasyon birimiydi. Page Object bu dört baskının altında mantıklıydı.

Bugün ekipler o bağlamı değil, deseni miras alıyor. Bu yazının tezi şu: Page Object bir soyutlama aracı değil, tekrar eden karmaşıklığı saklama aracıdır. Saklanacak karmaşıklık yoksa, desen sadece dosya sayısını artırır. Soyutlama bedava değildir; her sayfa sınıfı bir dolaylılık katmanı, bir bakım yüzeyi ve testi okuyan kişi için bir dosya atlaması demektir. Bu bedeli ödemeye değip değmediği, bağlama bakılarak karar verilecek bir mühendislik sorusudur — kültürel bir refleks değil.

Playwright’ın locator API’si neyi ortadan kaldırdı

Page Object’in klasik gerekçeleri dörttü. Üçü artık framework seviyesinde çözülmüş durumda:

Bekleme mantığının merkezileştirilmesi. Playwright locator’ları her eylemden önce actionability kontrolü yapar: görünür mü, stabil mi, etkinleştirilmiş mi, olayları alıyor mu. Explicit wait sarmalayıcısı yazmak için bir sayfa sınıfına ihtiyaç yok.

Stale element. Locator bir referans değil, bir tariftir. page.getByRole('button', { name: 'Kaydet' }) çağrıldığı anda DOM’u sorgulamaz; kullanıldığı anda çözer. DOM yeniden render olduğunda elde kalan bozuk referans diye bir şey yok, dolayısıyla “elementi yeniden bul” sarmalayıcılarına da gerek yok.

Seçici merkezileştirme. getByRole, getByLabel, getByTestId anlamsal seçicilerdir. div.container > ul:nth-child(3) > li.item > a gibi bir ifadeyi tek yerde tutmanın acil nedeni, ifadenin kendisi kırılgan olduğu içindi. Erişilebilirlik rolüne dayanan seçici, CSS sınıfı değiştiğinde kırılmaz.

Geriye tek gerçek gerekçe kalıyor: iş akışı tekrarı. Aynı altı adımlı kayıt formunu on testte doldurmak istemiyorsanız, bunu bir yere koymanız gerekir. Ama bu “bir yer”in sayfa sınıfı olması zorunlu değil.

Aynı testin iki hali:

// tests/checkout.pom.spec.ts — Page Object'li hal
// pages/LoginPage.ts
import { Page } from '@playwright/test';

export class LoginPage {
  constructor(private page: Page) {}

  get emailInput() { return this.page.getByLabel('E-posta'); }
  get passwordInput() { return this.page.getByLabel('Parola'); }
  get submitButton() { return this.page.getByRole('button', { name: 'Giriş yap' }); }

  async goto() {
    await this.page.goto('/giris');
  }

  async login(email: string, password: string) {
    await this.emailInput.fill(email);
    await this.passwordInput.fill(password);
    await this.submitButton.click();
  }
}

// pages/CartPage.ts
import { Page } from '@playwright/test';

export class CartPage {
  constructor(private page: Page) {}

  get checkoutButton() { return this.page.getByRole('button', { name: 'Siparişi tamamla' }); }
  get total() { return this.page.getByTestId('sepet-toplam'); }

  async goto() {
    await this.page.goto('/sepet');
  }
}
// tests/checkout.direct.spec.ts — doğrudan locator
import { test, expect } from '@playwright/test';

test('sepetteki tutar sipariş özetinde aynı görünür', async ({ page }) => {
  await page.goto('/giris');
  await page.getByLabel('E-posta').fill('ku*******@***ek.com');
  await page.getByLabel('Parola').fill('Parola123!');
  await page.getByRole('button', { name: 'Giriş yap' }).click();

  await page.goto('/sepet');
  const sepetToplam = await page.getByTestId('sepet-toplam').textContent();

  await page.getByRole('button', { name: 'Siparişi tamamla' }).click();
  await expect(page.getByTestId('siparis-toplam')).toHaveText(sepetToplam!.trim());
});

İkinci dosya tek başına okunur. Birinci versiyon aynı davranışı anlatmak için üç dosya ve iki sınıf kullanıyor; kazandığı tek şey login akışının tekrar kullanılabilir olması. Bu tek kazancı daha ucuz bir araçla elde etmek mümkün — aşağıda.

Page Object’in gerçekten kazandırdığı üç durum

Deseni toptan reddetmek de aynı refleksin tersidir. Page Object’in matematiği tuttuğu üç net durum var.

1. Aynı çok adımlı iş akışı üç veya daha fazla testte tekrar ediyorsa. Eşik tekrar sayısıdır, akışın “önemli” olması değil. İki testte geçen bir akışı soyutlamak erken; üçüncüsünde değişiklik maliyeti soyutlama maliyetini geçer. Tipik adaylar: login, çok adımlı checkout, sihirbaz formları, karmaşık filtre panelleri. Dikkat: burada soyutlanan sayfa değil akıştır — sınıf bunun yalnızca bir taşıyıcısıdır.

2. UI sık değişiyor ve değişiklik tek bir ekranda yoğunlaşıyor. Aktif geliştirme altındaki, sprint başına birden fazla kez yeniden düzenlenen ekranlar için merkezi bir dosya gerçek zaman kazandırır. Ölçüt, ekranın git geçmişidir. Altı aydır dokunulmamış bir ekran için merkezileştirme boş yere ödenmiş bir sigorta primidir.

3. Testleri domain bilgisi olan ama Playwright bilmeyen kişiler okuyacaksa. İş analisti veya ürün sahibi test dosyalarını inceliyorsa, await siparisSayfasi.kargoSec('Ertesi gün') satırı getByRole('radio', { name: 'Ertesi gün' }).check() satırından daha anlaşılırdır. Bu gerçek bir fayda ama yalnızca okuyucu profili gerçekten böyleyse geçerli. “Bir gün biri okur” varsayımıyla soyutlama yazmak, bedeli bugün ödeyip faydayı hiç almamaktır.

Page Object’in yük olduğu durumlar

Getter’dan ibaret sınıflar. get loginButton() { return this.page.getByRole('button', { name: 'Giriş yap' }); } sıfır değer üretir. Locator zaten tembeldir, zaten okunabilirdir, zaten tek satırdır. Bu getter yalnızca bir yeniden adlandırmadır ve karşılığında testi okuyan herkesi ikinci bir dosyaya gönderir. Sınıfın içinde davranış yoksa sınıf yoktur, isim listesi vardır.

Tek kullanımlı sayfa sınıfları. Bir testten çağrılan bir sayfa sınıfı, tanımı gereği tekrar problemi çözmez. Çözdüğü tek şey simetri kaygısıdır: “diğer sayfaların sınıfı var, bunun da olsun.”

Sayfa sınıfına sızan assertion’lar. async verifyLoginSuccess() gibi metotlar testin ne doğruladığını testten çıkarır. Artık test dosyası okunduğunda hangi koşulun kontrol edildiği görünmez; başarısız bir koşunun raporunda da “verifyLoginSuccess failed” dışında bilgi kalmaz. Assertion test niyetidir, test dosyasında durur.

Sayfa sınıfları arası kalıtım. En pahalısı budur:

// ANTI-PATTERN: her değişiklik tüm ağacı etkiler
abstract class BasePage {
  constructor(protected page: Page) {}
  get header() { return this.page.getByRole('banner'); }
  async logout() { await this.header.getByRole('button', { name: 'Çıkış' }).click(); }
}

abstract class AuthenticatedPage extends BasePage {
  get sidebar() { return this.page.getByRole('navigation'); }
  async openSettings() { await this.sidebar.getByRole('link', { name: 'Ayarlar' }).click(); }
}

class DashboardPage extends AuthenticatedPage {
  get widgets() { return this.page.getByTestId('widget'); }
}

Mekanik zarar şu: BasePage.header tanımı tek bir ekranda değiştiğinde değişikliği ya hiyerarşinin tepesinde yapıp tüm alt sınıfları riske atarsınız, ya da alt sınıfta override edip hiyerarşinin anlamını bozarsınız. Üstelik DashboardPage üzerinden logout() çağrıldığında metodun hangi seviyede tanımlı olduğunu bulmak için üç dosya açmak gerekir. Test kodunun en değerli özelliği okunabilirlikken, kalıtım tam da okunabilirliği satıyor. Üretim kodunda bu takası yapmanın gerekçeleri olabilir; test kodunda yoktur, çünkü test kodu yeniden kullanım için değil teşhis için yazılır. Kararsız bir testin kök nedenini ararken üç seviyeli bir sınıf ağacında gezinmek, teşhis süresini doğrudan uzatır — flaky testleri teşhis etme yazısında anlattığım gibi, burada belirleyici olan hatadan sebebe giden yolun kısalığıdır.

Alternatif: fixture tabanlı görev soyutlaması

Sayfayı değil görevi soyutlayın. Playwright’ın fixture sistemi, tekrar eden kurulumu testten çıkarmak için tam olarak bu işi yapar ve sınıf hiyerarşisi maliyeti getirmez.

// fixtures.ts
import { test as base, expect, Page } from '@playwright/test';

type Kullanici = { email: string; parola: string };

type Gorevler = {
  kullanici: Kullanici;
  girisYapilmisSayfa: Page;
  sepeteUrunEkle: (urunAdi: string) => Promise<void>;
};

export const test = base.extend<Gorevler>({
  kullanici: async ({}, use) => {
    const damga = Date.now();
    await use({ email: `test-${damga}@ornek.com`, parola: 'Parola123!' });
  },

  girisYapilmisSayfa: async ({ page, kullanici }, use) => {
    await page.goto('/giris');
    await page.getByLabel('E-posta').fill(kullanici.email);
    await page.getByLabel('Parola').fill(kullanici.parola);
    await page.getByRole('button', { name: 'Giriş yap' }).click();
    await expect(page.getByRole('banner')).toContainText(kullanici.email);
    await use(page);
  },

  sepeteUrunEkle: async ({ girisYapilmisSayfa }, use) => {
    await use(async (urunAdi: string) => {
      await girisYapilmisSayfa.goto('/urunler');
      await girisYapilmisSayfa.getByRole('searchbox', { name: 'Ara' }).fill(urunAdi);
      await girisYapilmisSayfa.getByRole('button', { name: 'Ara' }).click();
      await girisYapilmisSayfa
        .getByRole('listitem')
        .filter({ hasText: urunAdi })
        .getByRole('button', { name: 'Sepete ekle' })
        .click();
    });
  },
});

export { expect };
// tests/sepet.spec.ts
import { test, expect } from '../fixtures';

test('sepete eklenen ürün sipariş özetinde görünür', async ({
  girisYapilmisSayfa,
  sepeteUrunEkle,
}) => {
  await sepeteUrunEkle('Kablosuz Kulaklık');

  await girisYapilmisSayfa.goto('/sepet');
  await expect(girisYapilmisSayfa.getByTestId('sepet-satiri')).toHaveCount(1);

  await girisYapilmisSayfa.getByRole('button', { name: 'Siparişi tamamla' }).click();
  await expect(girisYapilmisSayfa.getByTestId('siparis-ozeti')).toContainText('Kablosuz Kulaklık');
});

Bu yapı Page Object’in tek gerçek faydasını verir: tekrar eden iş akışı bir yerde tanımlıdır. Karşılığında hiyerarşi yok, new çağrısı yok, instance yaşam döngüsü yönetimi yok. Fixture’lar birbirine bağımlılık üzerinden bağlanır (sepeteUrunEkle, girisYapilmisSayfa‘yı ister), bu da bağımlılığı kalıtımdan farklı olarak açıkça görünür kılar. Ayrıca her fixture kendi verisini üretebildiği için test izolasyonu doğal olarak sağlanır; bu konunun neden pazarlık edilemez olduğunu test verisi yönetimi yazısında ayrıntılandırdım.

Karar çerçevesi: hangi soyutlama ne zaman

Üç katman, artan maliyet sırasıyla: doğrudan locator (varsayılan) → fixture veya görev fonksiyonu (tekrar varsa) → Page Object sınıfı (durum tutması gereken karmaşık bileşen varsa).

Üçüncü katmanın gerçek kullanım alanı dardır ama vardır: kendi içinde durum taşıyan bileşenler. Sayfalanmış bir veri tablosu (geçerli sayfa numarası, aktif filtreler, seçili satırlar), çok adımlı bir sihirbaz (hangi adımda olduğunuz), sürükle-bırak bir düzenleyici. Bunlarda metotlar arasında taşınan durum olduğu için sınıf doğru araçtır.

Tekrar yoksa doğrudan locator; varsa durum taşınıyorsa veya okuyucu Playwright bilmiyorsa Page Object, değilse fixture.
Üç soyutlama katmanının maliyeti: Doğrudan locator, Fixture / görev fonksiyonu ve Page Object sınıfı seçenekleri Ne zaman kullanılır, Dosya atlaması ve Durum tutar mı kriterlerine göre karşılaştırılıyor. Doğrudan locator: Ne zaman kullanılır Varsayılan, Dosya atlaması Yok ve Durum tutar mı Hayır. Fixture / görev fonksiyonu: Ne zaman kullanılır 3+ testte tekrar, Dosya atlaması Bir ve Durum tutar mı Hayır. Page Object sınıfı: Ne zaman kullanılır Karmaşık bileşen, Dosya atlaması İki veya daha fazla ve Durum tutar mı Evet.

Mevcut Page Object katmanını kademeli söküp atmak

Elli sınıflık bir pages/ klasörünü tek commit’te silmek bir seçenek değil. Sıralı ve her adımı tek başına birleştirilebilir bir plan gerekir. Her adımın ölçütü aynı: tüm test süiti adımdan önce ve sonra yeşil, hiçbir testin gövdesi anlamca değişmemiş. Bu tür bir dönüşümde testlerin kendisi refactoring’in güvenlik ağıdır; süit zaten kararsızsa önce onu düzeltin, yoksa yeşil/kırmızı sinyali anlamsızdır.

Adım 1 — Getter-only sınıfları satır içine al. Yalnızca locator döndüren getter’lardan ibaret sınıfları tespit edin ve kullanım yerlerine açın. Bu adım saf mekaniktir, davranış değiştirmez, inceleme maliyeti düşüktür.

Adım 2 — Kalıtım hiyerarşisini düzleştir. Taban sınıflardaki metotları alt sınıflara kopyalayın, sonra taban sınıfları silin. Kopyalama geçici olarak tekrar üretir; amaç bu. Tekrar görünür olduğunda hangisinin gerçekten paylaşıldığı ortaya çıkar, varsayılan olarak paylaşılmış sayılanlar değil.

Adım 3 — Kalan sınıfları fixture’a çevir. Davranış içeren ama durum tutmayan sınıfları test.extend içindeki görev fonksiyonlarına taşıyın. Burada tipik olarak sınıf sayısı belirgin biçimde düşer, çünkü çoğu sayfa sınıfı aslında iki üç fonksiyondan ibarettir.

Adım 4 — Gerçekten kalması gerekenleri işaretle. Durum tutan bileşenler kalır. Bunları components/ gibi ayrı bir klasöre alın ve klasörün başına kalma gerekçesini yazan kısa bir README koyun. Gerekçesi yazılamayan sınıf kalmaz.

Bu sökümün yan etkisi genellikle koşu süresinde de görülür: fixture’a çevrilen kurulum adımları storageState veya API üzerinden kısa devre yapılabilir hale gelir. Büyük süitlerde bu kazanım küçümsenmeyecek boyuttadır; konuya test otomasyonunda performans optimizasyonu yazısında ayrıca değindim.

Takımda bu kararı nasıl savunursun

“Page Object best practice’tir” itirazı teknik bir argüman değil, otoriteye başvurudur. Verilecek cevap da teknik olmalı: her best practice bir bağlamın çözümüdür; bağlam değiştiyse pratiğin geçerliliği yeniden gösterilmelidir. Page Object’in dört gerekçesinden üçünü Playwright framework seviyesinde çözdü. Savunan taraftan istenecek şey basit: kalan hangi somut problemi çözdüğünü söylesin. “Düzen sağlıyor” cevabı yeterli değildir, çünkü düzen ölçülebilir bir fayda değil.

Tartışmayı zevk meselesi olmaktan çıkarmanın yolu, kararı kod inceleme kuralına dönüştürmektir. Yazılabilecek net eşikler:

  • Aynı çok adımlı akış en az üç ayrı test dosyasında geçmeden soyutlama yazılmaz.
  • Davranış içermeyen, yalnızca locator döndüren sınıf birleştirilmez.
  • Sayfa sınıfları expect çağrısı içermez.
  • Sayfa sınıfları arasında kalıtım kullanılmaz; paylaşılan davranış fixture’a taşınır.
  • Sınıf yerine fonksiyon tercih edilir; sınıf ancak metotlar arası durum taşınıyorsa açılır.

Bu beş madde yazılı olduğunda inceleme yorumu “bence gereksiz” değil, “üçüncü tekrar yok, kural 1” olur. Ekip ölçeği büyüdükçe bu tür yazılı eşiklerin değeri artar; test otomasyonunda ölçeklenebilirlik tam olarak bu tür kararların tekrar tekrar tartışılmamasıyla sağlanır.

Bundan sonra ne yapmalı

Bugün yapılacak iki somut iş var.

Birincisi: mevcut repoda pages/ altındaki her sınıfı açın ve iki şeyi sayın — kaçı yalnızca getter içeriyor, kaçı tek bir test dosyasından çağrılıyor. Bu iki kümenin birleşimi, silinmeye hazır olan kısımdır. Sayım işini gözle yapın; satır sayısı değil, sınıf başına “hangi problemi çözüyor” sorusunun cevabı önemli.

İkincisi: yazacağınız bir sonraki testte varsayılanı doğrudan locator yapın. Soyutlamayı aynı akış üçüncü kez karşınıza çıkana kadar yazmayın. Üçüncü kez çıktığında da önce fixture deneyin; sınıfı yalnızca durum taşıma ihtiyacı kanıtlandığında açın. Soyutlamayı sonradan eklemek kolaydır, yanlış soyutlamayı sökmek pahalıdır — sıralamayı buna göre kurun.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

Bu site istenmeyenleri azaltmak için Akismet kullanır. Yorum verilerinizin nasıl işlendiğini öğrenin.