Test Otomasyonu İçin Tasarım Desenleri

Test otomasyonu, günümüzde hızla gelişen ve karmaşıklaşan yazılım projelerinde kritik bir rol oynamaktadır. Bu süreçte, sadece test senaryolarını yazmakla kalmayıp, aynı zamanda bu senaryoların sürdürülebilir, esnek ve bakımının kolay olmasını sağlamak da büyük bir önem taşımaktadır. İşte tam da bu noktada, tasarım desenleri devreye girer.

Bu yazımızda, test otomasyon projelerinde karşılaşılan zorluklara yaratıcı çözümler getiren tasarım desenlerini ele alacağız. Her bir desen, test senaryolarının yazımından, bakımına ve genişletilmesine kadar bir dizi avantaj sunarak, otomasyon sürecini daha yönetilebilir ve güçlü kılacaktır.

Single Responsibility Principle (SRP)

Single Responsibility Principle (SRP), yazılım geliştirmenin temel prensiplerinden biridir. Bu prensip, Robert C. Martin’in SOLID prensiplerinden biridir ve yazılım tasarımının sürdürülebilirliği ve bakımı için önemli bir rol oynar.

Bir sınıfın yalnızca bir sorumluluğu olması gerektiğini belirtir. Yani bir sınıf, yalnızca bir nedenden dolayı değişmelidir ve bu genellikle sınıfın bir işlevi veya görevi olmalıdır. Eğer bir sınıf birden fazla sorumluluğu üstlenirse, bu sınıfın değişikliklere daha fazla duyarlı olması ve bakımının zorlaşması muhtemeldir.

Bu prensip, kodun daha modüler, sürdürülebilir ve anlaşılır olmasına katkıda bulunur. Bir sınıfın sadece belirli bir işlevselliği ele alması, kodun daha az bağımlılık içermesini ve daha kolay test edilebilir olmasını sağlar. Ayrıca, değişikliklerin sınırlı bir etki alanına sahip olmasını sağlayarak kodun daha güvenilir hale gelmesine yardımcı olur.

KISS(Keep It Simple Stupid)

KISS, yazılım geliştirmede ve genel olarak tasarımda önemli bir prensiptir. KISS prensibi, bir sistem, tasarım veya sürecin basit ve anlaşılır tutulması gerektiğini vurgular. Bu prensip, gereksiz karmaşıklıklardan kaçınılmasını ve tasarımın mümkün olan en basit ve etkili şekilde yapılmasını önerir.

DRY (Don’t Repeat Yourself)

DRY, yazılım geliştirmenin temel prensiplerinden biridir. Bu prensip, yazılım kodunun içerdiği bilgilerin tekrarlanmamasını ve aynı mantığın birden fazla yerde kodlanmamasını önerir. Bu, bir kod parçasının veya işlevselliğin bir defa yazılıp, daha sonra ihtiyaç duyulduğunda tekrar kullanılması anlamına gelir.

DRY prensibi, yazılım geliştirmenin yanı sıra bakımını da kolaylaştırarak hata olasılığını azaltmaya çalışır. Tekrarlanan kod blokları, bir hata düzeltildiğinde veya bir değişiklik yapıldığında, her tekrarlanan yerde güncellenmesi gerektiğinden, bu durum hem zaman alıcı hem de hata yapma olasılığını artırıcı bir faktördür. Bu nedenle, DRY prensibi, kodun okunabilirliğini artırır, bakımını kolaylaştırır ve geliştirme sürecini daha verimli hale getirir.

Tasarım desenlerine geçmeden önce bu üç prensipten ufakta olsa bahsetmek istedim çünkü birçok tasarım deseni bu temel prensipleri destekler.

Tasarım Deseni Nedir?

Tasarım deseni (design pattern), yazılım geliştirme sürecinde karşılaşılan genel problemlere yönelik tekrar kullanılabilir çözüm önerileridir. Bu desenler, daha önce ortaya çıkan benzer problemlere yönelik test edilmiş çözümleri içerir. Yazılım projelerinin tasarım aşamasında belirli problemleri ele almak için kullanılabilir ve birçok yazılım mühendisi tarafından iyi bir uygulama pratiği olarak kabul edilir.

Tasarım desenleri üç ana kategoride toplanır:

  1. Creational Patterns (Yaratımsal Desenler): Nesne yaratma işlemleri ile ilgilenir ve nesne yaratma süreçlerini soyutlar. Bu desenler, nesnelerin nasıl yaratılacağına dair en iyi yöntemleri tanımlar.
  2. Structural Patterns (Yapısal Desenler): Sınıflar ve nesneler arasındaki ilişkileri düzenler ve sistemdeki bileşenleri birleştirir. Bu desenler, sınıfların bir araya getirilmesi ile ilgilenir.
  3. Behavioral Patterns (Davranışsal Desenler): Nesneler arasındaki işbirliği ve sorumlulukları düzenler. Bu desenler, nesnelerin nasıl birbirleriyle etkileşime geçeceğini tanımlar.

Tasarım desenleri, yazılım geliştirmeye yönelik bir dizi avantaj sağlasa da, yanlış bir şekilde seçilen desenlerin bazı olumsuz etkileri olabilir. Bilinçli bir şekilde tasarım deseni seçmenin önemli olduğu durumlar arasında karmaşıklık, performans maliyeti ve gereksiz soyutlamalar bulunmaktadır. İhtiyaca uygun olmayan tasarım desenleri kullanıldığında, kodun anlaşılabilirliği azalabilir ve projeye ek maliyet getirebilir.

Test otomasyonunda çeşitli zorlukların üstesinden gelmek ve testlerin kalitesini, sürdürülebilirliğini artırmak için birçok tasarım deseni yaygın bir şekilde kullanılmaktadır. Bu yazımda, size yedi özel tasarım deseni sunacağım. Tabii ki, bunların dışında birçok tasarım deseni mevcut; ancak, test otomasyonu için uygulanabilirliği düşük olanları da göz önüne alarak seçimimi yaptım. Lafı daha fazla uzatmadan, popüler olan ilk tasarım desenimizle başlayalım.

Page Object Design Pattern

Test edilecek her bir web sayfası için bir sınıf oluşturarak okunabilirliği ön planda tutan en popüler test otomasyon tasarım desenlerinden bir tanesidir. Her sayfa nesnesi, uygulama içindeki bir sayfayı temsil eder ve bu sayfada sunulan butonlar, onay kutuları veya metin alanları (örneğin, kullanıcı adı, parola alanları) gibi web öğeleriyle etkileşim kurmaya yönelik methodları içerir. Bu tasarım deseni, nesnelerin nasıl bir araya getirileceğini ve ilişkilendirileceğini tanımladığı için Structural Design Pattern grubuna dahil edilebilir.

Page Object Model (POM) tasarım deseninin diğer alanlarda da birçok benzer örneği bulunabilir. İşte birkaç örnek:

  • Televizyon Uzaktan Kumandası: Bir televizyon uzaktan kumandası, televizyonun farklı işlevlerini kontrol etmek için kullanılır. Her düğme, belirli bir işlevi temsil eder. Bu, bir web sayfasının farklı özelliklerini temsil eden Page Objects’a benzer. Her Page Object, sayfanın belirli bir özelliğini kontrol etmek için kullanılır.
  • Giysi Dolabı: Giysilerinizi belirli bir düzen içerisinde yerleştirmeniz durumunda aradığınızı daha kolay bulabilirsiniz bu da size zamanda tasarruf sağlar. Bir otomasyon projesinde sayfa sınıflarınızı ve test sınıflarınızı ayrı oluşturmak size yine zaman kazancı ve bakım kolaylığı sağlayacaktır.
  • Ev ve Odalar: Bir ev, genellikle bir web uygulamasına benzetilebilir. Her oda, bir sayfayı temsil eder. POM’da her sayfa için bir sayfa nesnesi oluşturmak, o sayfanın özelliklerini ve davranışlarını içerir.

Nasıl İmplemente Edilir?

  1. Her bir web sayfasını temsil eden sınıfları oluşturun.
  2. Sayfalardaki etkileşim kuracağınız elemanları (text kutuları, düğmeler, bağlantılar vb.) bu sınıfta tanımlayın.
  3. Bu sınıflarda sayfa üzerinde gerçekleştirilen işlemleri (metodları) tanımlayın.
  4. WebDriver örneğini test sınıfından sayfa nesnesi sınıflarına iletmek için bir yöntem kullanın. Genellikle bu, constructor veya getter-setter metodları aracılığıyla yapılır.
  5. Test senaryolarında sayfa nesnelerini kullanarak sayfa üzerindeki işlemleri gerçekleştirin.

Bir e-ticaret uygulamasında, ana sayfa, ürün listeleme sayfası, sepet sayfası, ödeme sayfası ve iletişim sayfası gibi farklı sayfalar bulunmaktadır. Her bir sayfa, içerisinde bir dizi işlevi barındırmaktadır, örneğin sepet sayfası için; sepetten çıkartma, kupon ekleme, adet güncelleme vb. Bu tasarım deseni, her bir sayfa için ayrı bir sınıf oluşturmayı önerir. İlgili sayfaya özgü işlevler bu sınıfta metod olarak tanımlanır. Ayrıca, sayfa üzerinde etkileşime girilecek öğelerin konum belirleyicileri de yine bu sınıfta tanımlanır. Bu yaklaşım, test kodunu yazarken okunabilirlik, sürdürülebilirlik ve bakım kolaylığı sağlar.

Page Object Tasarım Deseni Sınıf Diyagramı

Verdiğimiz örneği kod üzerinden inceleyelim.

public class SepetSayfasi {
    private WebDriver driver;

    private By urunAdetInput = By.xpath("//input[@class='urun-adet']");
    private By sepettenCikarButton = By.xpath("//button[@class='sepetten-cikar']");
    private By kuponEkleInput = By.xpath("//input[@id='kupon-kodu']");
    private By kuponEkleButton = By.xpath("//button[@id='kupon-ekle']");

    public SepetSayfasi(WebDriver driver) {
        this.driver = driver;
    }

    public void urunAdetiniGuncelle(int yeniAdet) {
        driver.findElement(urunAdetInput).clear();
        driver.findElement(urunAdetInput).sendKeys(String.valueOf(yeniAdet));
    }

    public void sepettenCikar() {
        driver.findElement(sepettenCikarButton).click();
    }

    public void kuponEkle(String kuponKodu) {
        driver.findElement(kuponEkleInput).sendKeys(kuponKodu);
        driver.findElement(kuponEkleButton).click();
    }
}
  • E-ticaret uygulamamızda Sepet sayfasını temsil eden SepetSayfasi adında bir sınıf oluşturduk.
  • By yöntemiyle sayfada yer alan konum belirleyicileri Xpath türünde tanımladık.
  • Sayfada yer alan fonksiyonlar için birer method oluşturduk.
public class TestSenaryosu {
    private WebDriver driver;
    private SepetSayfasi sepetSayfasi;

    @BeforeClass
    public void setup() {
        driver = new ChromeDriver();
        driver.get("https://www.example.com/sepet");
        sepetSayfasi = new SepetSayfasi(driver);
    }

    @Test
    public void sepetIslemleriTest() {
        sepetSayfasi.urunAdetiniGuncelle(3);
        sepetSayfasi.kuponEkle("INDIRIM50");
        sepetSayfasi.sepettenCikar();
    }

    @AfterClass
    public void tearDown() {
        driver.quit();
    }
}

Bu tasarım deseni, test senaryolarının sayfa nesneleri üzerinden yönetilmesini sağlar. Bu şekilde, test kodunun okunabilirliği artar, bakımı daha kolay hale gelir ve bir sayfa üzerinde yapılan değişiklikler test kodlarına minimum etki yapar.


Factory Design Pattern

Nesne oluşturma süreçlerini soyut bir arayüzle (interface veya abstract class) tanımlayarak ve bu arayüzü uygulayan alt sınıfları kullanarak, nesne yaratma işlemlerini birleştiren bir tasarım desenidir.

Bu tasarım deseni, test otomasyon projelerini daha esnek ve genişletilebilir hale getirir, çünkü yeni sınıflar eklemek veya mevcut sınıfları değiştirmek daha kolay olur. Ayrıca nesne yaratma süreçlerini soyutlayarak kodun daha az bağımlı, daha modüler ve daha bakımı kolay olmasını sağlar. Nesne oluşturmayı kontrol ettiği için Creational Design Pattern grubuna dahildir.

Factory tasarım deseninin diğer alanlarda da birçok benzer örneği bulunabilir. İşte birkaç örnek:

  • Otomobil Fabrikası ve Modelleri: Otomobil üretimi, Factory tasarım desenine benzer bir yaklaşımı kullanabilir. Bir otomobil fabrikası, belirli bir model ve özelliklere sahip araçları üretmek için bir Factory tasarım deseni kullanabilir. Her model için bir fabrika sınıfı, o modelin nesnelerini oluşturabilir.
  • Restoran ve Menü Siparişi: Bir restoran, belirli bir menü kategorisine dayalı olarak sipariş alabilir. Factory tasarım deseni burada, bir menü öğesinin sipariş edilmesi durumunda ilgili menü öğesinin nesnesini oluşturabilir. Örneğin, pizza siparişi verildiğinde PizzaFactory, pizza nesnesini oluşturabilir.
  • E-ticaret ve Ürün Kategorileri: Bir e-ticaret platformu, kullanıcı bir ürün kategorisi seçtiğinde, bu kategoriye özgü bir ürün nesnesi oluşturmak için Factory tasarım desenini kullanabilir. Örneğin, ElektronikFactory sınıfı, elektronik ürünlerin nesnelerini oluşturabilir.

Nasıl İmplemente Ederiz?

  1. Oluşturulacak nesnelerin ortak arayüzünü veya soyut sınıfını ve methodunu oluşturun.
  2. Ortak arayüzü veya soyut sınıfı implemente/extend edecek somut sınıfları oluşturun ve implemente edin.
  3. Factory sınıfınızı oluşturun.
  4. Test senaryonuzu ve test methodlarınızı oluşturun.
Factory Tasarım Deseni Sınıf Diyagramı

Test senaryolarında kullanılacak test verilerini oluşturmak ve yönetmek zor olabilir. Her senaryo için farklı test verilerine ihtiyaç duyulabilir, bu da kodun karmaşıklığını ve bakım zorluğunu artırabilir. Factory tasarım desenini kullanarak bu sorunu nasıl çözebileceğimize bir göz atalım.

public interface IReadTestData {
    Map<String, String> readData();
}
  • IReadTestData arayüzünü implemente eden sınıflar, arayüzde tanımlanan metodu kendi özel ihtiyaçlarına göre implemente etmek zorundadır.
  • Yani, bu arayüzü implemente eden bir sınıf, readData metodunu içermeli ve bu metodu nasıl gerçekleştireceğine dair kendi özel veri okuma yöntemini kodlamalıdır.
public class ExcelRead implements IReadTestData {
    @Override
    public Map<String, String> readData() {
        System.out.println("Excel file reading...");
        return new HashMap<>();
    }
}
public class JsonRead implements IReadTestData {
    @Override
    public Map<String, String> readData() {
        System.out.println("JSON file reading...");
        return new HashMap<>();
    }
}
public class TxtRead implements IReadTestData{
    @Override
    public Map<String, String> readData() {
        System.out.println("TXT file reading...");
        return new HashMap<>();
    }
}

Bu sınıflar, IReadTestData arayüzü tarafından belirlenen readData metodu aracılığıyla test verilerini farklı kaynaklardan okuma yeteneklerini sağlarlar. Gerçek uygulamada, bu sınıfların içerisinde dosya okuma veya API çağrıları gibi işlemler gerçekleştirilerek test verileri elde edilir ve döndürülür.

public class ReadFactory {
    private static final Map<String, Supplier<IReadTestData>> MAP = new HashMap<>();

    static {
        MAP.put("excel", ExcelRead::new);
        MAP.put("txt", TxtRead::new);
        MAP.put("json", JsonRead::new);
    }

    public static IReadTestData read(String method) {
        Supplier<IReadTestData> testDataSupplier = MAP.get(method.toLowerCase());
        if (testDataSupplier == null) {
            throw new IllegalArgumentException("Not supported test data read method");
        }
        return testDataSupplier.get();
    }
}

ReadFactory, farklı test verisi okuma yöntemlerini destekleyen sınıfların örneklerini yaratmak için kullanılır.

  • MAP adında bir static ve final özellik (harita) bulunur.
  • Bu harita, desteklenen test verisi okuma yöntemleri (örneğin, “excel”, “txt”, “json”) ile bu yöntemleri temsil eden sınıfları eşleştirir.
  • Harita, Supplier sınıfından gelen referanslarla doldurulur. Supplier, bir sınıfın yeni bir örneğini üretmek için kullanılır.
  • public static IReadTestData read(String method): test verisi okuma yöntemine (örneğin, “excel”, “txt”, “json”) göre ilgili veri okuma sınıfını oluşturarak geri döndürür.
  • Supplier<IReadTestData> türündeki bir değişkene, MAP haritasından ilgili test verisi okuma sınıfının üretici (Supplier) referansını alır.
public class LoginTest {
    @Test
    public void loginTest() {
        IReadTestData data = ReadFactory.read("excel");
        Map<String, String> excelData = data.readData();
    }
}

IReadTestData data = ReadFactory.read("excel"); ReadFactory kullanılarak, “excel” test verisi okuma yöntemini temsil eden bir sınıfın örneği oluşturularak excel veri okuma yöntemi kullanılır. İlerleyen zamanlarda ben bu test methodumda excel veri okuma yöntemi yerine json kullanmak istersem tırnak içerisinde yer alan “excel” ibaresini “json” olarak değiştirmem yeterli olacaktır.

Factory tasarım deseni, özellikle bir uygulamada hangi nesnelerin oluşturulacağını belirlemenin ve değiştirmenin kolay olması gerektiği durumlarda kullanışlıdır. Ayrıca, kodun daha modüler ve bakımı daha kolay hale gelmesine yardımcı olabilir. Bu desen, yeni sınıflar eklenmesi veya mevcut sınıfların değiştirilmesi gibi durumlar için esnek bir yapı sunar.


Singleton Design Pattern

Bir sınıfın yalnızca bir örneğini oluşturmayı ve bu örneğe global bir erişim noktası belirlemeyi amaçlayan bir tasarım desenidir. Singleton pattern, belirli durumlarda yalnızca bir örneğe ihtiyaç duyulan sınıflarda kullanılır ve bu örneğe tüm kodun genelinden kolayca erişilmesini sağlar. Nesne oluşturmayı kontrol ettiği için Creational Design Pattern grubuna dahildir.

Singleton tasarım deseninin diğer alanlarda da birçok benzer örneği bulunabilir. İşte birkaç örnek:

  • Hükümetler: Bir ülkede genellikle yalnızca bir hükümet bulunur ve bu hükümetin yetkileri ve sorumlulukları vardır. Bu, Singleton desenine benzer çünkü bir ülkede birden fazla hükümet olması durumunda, yetkilerin ve sorumlulukların çakışması kaosa neden olabilir.
  • Bilgisayar Yazıcıları: Bir ağdaki bir yazıcı genellikle Singleton desenine benzer. Birçok kullanıcı aynı yazıcıyı kullanabilir, ancak yazdırma işlemleri genellikle sırayla gerçekleştirilir. Bu, Singleton deseninin global erişim noktası sağlama özelliğine benzer.
  • Dünya Saati: Dünya genelindeki tüm saatler, Coordinated Universal Time (UTC) olarak bilinen tek bir referans saatine dayanır. Bu, Singleton desenine benzer çünkü tüm saatlerin aynı referansı kullanması, saatler arasında tutarlılık sağlar.

Birden çok test senaryosu çalıştırıldığında her test senaryosu için ayrı bir WebDriver örneği oluşturmak, kaynakların çabuk tükenmesine ve performans sorunlarına neden olabilir. Singleton pattern kullanarak yalnızca bir WebDriver örneği oluşturarak, kaynak kullanımını optimize edebilir ve test senaryolarının daha hızlı çalışmasını sağlayabilirsiniz.

Aşağıda, bir web sitesine göz atmayı sağlayan temel bir test metodu bulunmaktadır. Başka bir sayfayı test etmek için yeni bir test sınıfı oluştururuz ve aynı adımları orada da uygularız. Testleri ayrı ayrı çalıştırdığınızda herhangi bir sorunla karşılaşmazsınız; ancak her iki test sınıfını da aynı anda çalıştırmak istediğinizde, her iki test sınıfı için de birer tarayıcının açıldığını göreceksiniz, bunu istemeyiz değil mi ? Aslında ihtiyacımız olan şey, tüm test senaryolarımızı koşarken tek bir WebDriver örneğidir. Singleton tasarım deseni tam olarak bu noktada devreye giriyor.

Singleton Tasarım Deseni Örnek Sınıfı Diyagramı
WebDriver driver;
    @Test
    public void singletonPatternTest(){
        driver = new ChromeDriver();
        driver.get("https://demo.yasinalbakir.net");
    }

Nasıl İmplemente Ederiz?

  1. Sınıfın dışından erişilemeyen bir private constructor tanımlanır. Bu, sınıfın dışında doğrudan örneğinin oluşturulmasını engeller.
  2. Sınıf içinde bir static örnek (instance) alanı tanımlanır. Bu örnek, sınıfın yalnızca bir kere oluşturulmasını sağlar.
  3. Yine sınıf içinde bir static method tanımlanır. Bu method, sınıfın örneğine erişim sağlar. Eğer örnek zaten oluşturulmuşsa bu örneği döndürür, oluşturulmamışsa önce örneği oluşturur ve sonra döndürür.
public class DriverManager {
    private static WebDriver driver;

    private DriverManager() {
        driver = new ChromeDriver();
    }

    public static WebDriver getInstance() {
        if (driver == null) {
            new DriverManager();
        }
        return driver;
    }

    public static void closeInstance() {
        if (driver != null) {
            driver.quit();
            driver = null;
        }
    }
}
  1. DriverManager sınıfı, WebDriver nesnesini singleton tasarım deseniyle oluşturan ve yöneten bir sınıftır.
  2. private static WebDriver driver: WebDriver nesnesini depolamak için kullanılan private bir static alan. Bu, sınıfın genelinde erişilebilir olmasını sağlar.
  3. private DriverManager(): Private bir kurucu method. Sınıfın dışında bu kurucu methoda doğrudan erişilemez, bu nedenle dışarıdan bir nesne oluşturulamaz.
  4. public static WebDriver getInstance(): Singleton tasarım desenini uygulayan statik bir method. Bu method, WebDriver örneğini kontrol eder ve örneği oluşturur veya mevcut örneği döndürür.
  5. Eğer bir örnek oluşturulmamışsa DriverManager sınıfının kurucu metodunu çağırarak bir örnek oluşturulmaya çalışılır. Ancak, bu örnek sınıfın içindeki özel bir kurucu methoddur ve bu şekilde doğrudan çağrılamaz. Bu nedenle yeni bir örnek oluşturulsa bile driver alanı hiçbir zaman güncellenmez ve daima null olarak kalır.
  6. return driver;: Her durumda, mevcut veya oluşturulan örneği döndürür.
  7. public static void closeInstance(): WebDriver örneğini kapatmak ve mevcut örneği temizlemek için kullanılan bir methoddur.
public class SingletonTest {
    WebDriver driver;

    @BeforeClass
    public void setup() {
        driver = DriverManager.getInstance();
    }

    @Test
    public void singletonPatternTest() {
        driver.get("https://demo.yasinalbakir.net");
    }

    @AfterClass
    public void teardown() {
        DriverManager.closeInstance();
    }
}

Yukarıda test sınıfı içerisinde DriverManager kullanımını gösterdim, getInstance() methoduyla tüm testler arasında tek bir WebDriver örneğini paylaşabilirsiniz.

Bu örnek dışında Singleton tasarım desenini başka nerelerde kullanabiliriz şeklinde bir soru aklınıza düştüyse;

Test senaryolarında kullanılan konfigürasyon bilgilerini (örneğin, tarayıcı türü, URL, kullanıcı adı, şifre) tek bir yerden yönetmek istiyorsanız yine bu tasarım desenini kullanarak konfigürasyon sınıfını tek bir noktadan yönetebilir ve test senaryolarında bu bilgilere kolayca erişebilirsiniz.


Facade Design Pattern

Facade Tasarım Deseni, karmaşık bir alt sistemi basitleştirmek için tek bir arayüz sağlayan yapısal bir tasarım desenidir. Test kodunun okunabilirliğini ve bakımını kolaylaştırmak, kod tekrarını azaltmak ve web sayfalarındaki değişikliklere kolayca uyum sağlamak için kullanılabilir.

Structural Design Pattern kategorisine aittir çünkü yazılım sistemlerindeki alt sistemlerin düzenini belirlemek ve bu alt sistemler arasındaki etkileşimi yönetmek için kullanılır.

Facade tasarım deseninin diğer alanlarda da birçok benzer örneği bulunabilir. İşte birkaç örnek:

  • Ev Sinema Sistemi: Evdeki birçok cihaz (televizyon, klima, ses sistemleri) farklı marka ve modellere sahip olabilir. Ancak bir uzaktan kumanda, kullanıcıya basit ve tek bir noktadan bu cihazlara erişim sağlar. Uzaktan kumanda, kullanıcıya karmaşıklığı gizleyen bir Facade işlevi görebilir.
  • Bir Araba’nın İç Mekânı: Arabanın iç mekânı, birçok alt sistem içerir: klima, müzik sistemi, gösterge paneli vb. Ancak kullanıcılar genellikle sadece direksiyon, vites ve temel kontrollerle etkileşime girer. Bu durumda, araç iç mekânının basitleştirilmiş bir arayüzü, kullanıcının karmaşıklığı azaltan bir Facade görevi görebilir.
  • Ödeme İşlemleri: Bir e-ticaret platformunda ödeme yapmak için kullanıcılar kredi kartı bilgilerini girer, fatura adreslerini belirtir ve diğer bilgileri sağlar. Ancak bu karmaşıklığı gizlemek için bir ödeme arayüzü kullanılabilir. Bu arayüz, ödeme işlemlerini gerçekleştirmek için alt sistemleri (banka entegrasyonu, kart doğrulama vb.) kullanabilir.

Nasıl İmplemente Edilir?

  • Alt sistemdeki sınıfları ve fonksiyonlarını tanımlayın.
  • Facade sınıfını oluşturun. Bu sınıf, alt sistemdeki sınıflarla etkileşim kurmak için gerekli metotları içerir.
  • Facade sınıfını kullanarak, alt sistemdeki karmaşıklığı gizleyin ve basit bir arayüz sunun.
  • İsteğe bağlı olarak, farklı ihtiyaçlara göre farklı Facade sınıfları oluşturabilirsiniz.

Aşağıdaki örneğimizle bu tasarım desenini anlatmaya çalışayım,

XYZ Bank adında tamamen hayal ürünü olan bir banka için hazırlanmış web uygulamamız var, sırasıyla banka yöneticisi olarak giriş yapacağız, yeni bir müşteri kaydı oluşturup müşteriye dolar hesabı tanımlayacağız.

Aşağıda sınıf diyagramını paylaştığım örneğimizde, çeşitli sayfaları ziyaret etme ve onunla etkileşime geçme karmaşıklığını gizlemek için bir facade sınıfı ve gerekli tüm iş mantığını bir yöntemde oluşturabiliriz.

Facade Tasarım Deseni Örnek Sınıf Diyagramı

Yukarıdaki senaryoyu Facade Tasarım Deseni ile otomatikleştirmek için POM Tasarım Deseninde yaptığımız gibi konumlayıcılar ve sayfa methodları için LoginPage, HomePage, CustomerAddPage ve OpenAccountPage sayfa sınıflarını oluşturuyoruz.

public class LoginPage {
    WebDriver driver;
    By btnEnter = By.xpath("//button[normalize-space()='Bank Manager Login']");

    public LoginPage(WebDriver driver) {
        this.driver = driver;
    }

    public void signIn() {
        driver.get("https://www.way2automation.com/angularjs-protractor/banking/#/login");
        driver.findElement(btnEnter).click();
    }
public class HomePage {
    WebDriver driver;
    By btnAddCustomer = By.xpath("//button[normalize-space()='Add Customer']");
    By btnOpenAccount = By.xpath("//button[normalize-space()='Open Account']");

    public HomePage(WebDriver driver) {
        this.driver = driver;
    }

    public void navigateCustomerAddPage() {
        driver.findElement(btnAddCustomer).click();
    }

    public void navigateOpenAccountPage() {
        driver.findElement(btnOpenAccount).click();
    }

}
public class CustomerAddPage {
    WebDriver driver;
    By txtFirstName = By.xpath("//input[@placeholder='First Name']");
    By txtLastName = By.xpath("//input[@placeholder='Last Name']");
    By txtPostCode = By.xpath("//input[@placeholder='Post Code']");
    By btnSave = By.xpath("//button[@type='submit']");

    public CustomerAddPage(WebDriver driver) {
        this.driver = driver;
    }

    public void createNewCustomerRecord(String name, String lastname, String postcode) {
        driver.findElement(txtFirstName).sendKeys(name);
        driver.findElement(txtLastName).sendKeys(lastname);
        driver.findElement(txtPostCode).sendKeys(postcode);
        driver.findElement(btnSave).click();
    }

    public String getCustomerId() {
        Alert alert = driver.switchTo().alert();
        String alertMessage = alert.getText();
        return ":".split(alertMessage)[1].trim();
    }
}
public class OpenAccountPage {

    WebDriver driver;
    Select select;
    By userSelect = By.xpath("//select[@id='userSelect']");
    By currencySelect = By.xpath("//select[@id='currency']");

    By btnProcess = By.xpath("//button[contains(text(),'Process')]");

    public OpenAccountPage(WebDriver driver) {
        this.driver = driver;
    }

    public void openDollarAccount(String user) {
        select = new Select((WebElement) userSelect);
        select.selectByVisibleText(user);

        select = new Select((WebElement) currencySelect);
        select.selectByValue("Dollar");

        driver.findElement(btnProcess).click();
    }
}

Test sınıflarının kullanabileceği tüm karmaşık fonksiyonları içeren OpenDollarAccountFacade sınıfını oluşturuyoruz.

public class OpenDollarAccountFacade {
    private LoginPage loginPage;
    private HomePage homePage;
    private CustomerAddPage customerAddPage;
    private OpenAccountPage openAccountPage;

    public OpenDollarAccountFacade(LoginPage loginPage,
                                   HomePage homePage,
                                   CustomerAddPage customerAddPage,
                                   OpenAccountPage openAccountPage) {
        this.loginPage = loginPage;
        this.homePage = homePage;
        this.customerAddPage = customerAddPage;
        this.openAccountPage = openAccountPage;
    }

    public void openingDollarAccountForNewCustomer(User user) {
        loginPage.signIn();
        homePage.navigateCustomerAddPage();
        customerAddPage.createNewCustomerRecord(user.getName(), user.getLastName(), user.getPostCode());
        openAccountPage.openDollarAccount(customerAddPage.getCustomerId());
    }
}

Son olarak test sınıfımızı oluşturarak, yeni müşterimiz için dolar hesabı oluşturma senaryomuzu hazırlıyoruz.

public class OpenAcountTest {
    OpenDollarAccountFacade openDollarAccountFacade;
    LoginPage loginPage;
    HomePage homePage;
    CustomerAddPage customerAddPage;
    OpenAccountPage openAccountPage;
    User user;
    WebDriver driver;

    @BeforeTest
    public void setup() {
        driver = new ChromeDriver();
        loginPage = new LoginPage(driver);
        homePage = new HomePage(driver);
        customerAddPage = new CustomerAddPage(driver);
        openAccountPage = new OpenAccountPage(driver);
        openDollarAccountFacade = new OpenDollarAccountFacade(loginPage, homePage, customerAddPage, openAccountPage);

    }

    @Test
    public void openDollarAccountTest() {
        openDollarAccountFacade.openingDollarAccountForNewCustomer(
                new User("selenium", "driver", "06601"));
        Assert.assertEquals(driver.switchTo().alert().getText(), "Account created successfully with account Number");
    }
}

Test otomasyon projelerinde karmaşık bir alt sistemle iletişim kurmak veya farklı bileşenler arasındaki etkileşimi yönetmek genellikle zordur. Facade tasarım deseni, bu karmaşıklığı gizleyerek test otomasyon kodunun daha okunabilir, sürdürülebilir ve yönetilebilir olmasına yardımcı olabilir.


Fluent Design Pattern

Fluent tasarım deseni, bir yazılım uygulamasının okunabilirliğini, sürdürülebilirliğini ve anlaşılabilirliğini artırmak amacıyla kullanılan bir tasarım desenidir. Bu tasarım deseni, bir nesnenin metodlarını birleştirerek zincirleme bir şekilde çağrılmasını sağlar. Test otomasyon senaryolarının anlaşılabilirliğini artırmak için yaygın olarak kullanılmaktadır.

Fluent tasarım deseni, nesnelerin birbirleriyle olan etkileşimini daha açık ve anlaşılır bir hale getirmek amacıyla kullanılan bir davranışsal (behavioral) tasarım deseni olarak kabul edilir.

Fluent tasarım deseninin diğer alanlarda da birçok benzer örneği bulunabilir. İşte birkaç örnek:

  • ORM (Object-Relational Mapping) Kütüphaneleri: Fluent Interface, ORM kütüphanelerinde sıkça görülebilir. Örneğin, Entity Framework veya Hibernate gibi ORM kütüphaneleri, sorguları zincirleme yöntemiyle oluşturmak için Fluent Interface özelliklerini kullanabilir. Bu, sorgu oluşturmayı daha okunabilir ve esnek hale getirir.
  • Test Framework’leri ve Assertion Kütüphaneleri: Test framework’leri ve assertion kütüphaneleri, test durumlarını oluşturmayı ve doğrulamayı daha okunabilir ve esnek hale getirmek için Fluent Interface kullanabilir.

Nasıl İmplemente Edilir?

  • İlk olarak, test senaryonuzun başlangıcındaki sayfayı temsil eden bir sınıf oluşturun. Bu sınıf, sayfa üzerindeki elementlere erişim sağlamalıdır, Her method, işlemi gerçekleştirdikten sonra kendisini (this) dönmelidir, böylece zincirleme şeklinde kullanılabilir.
  • return new ........Page(driver) ifadesi, Fluent Design Pattern içerisinde bir sayfa üzerindeki bir işlemi gerçekleştirdikten sonra, bir sonraki sayfaya geçiş yapmayı temsil eder. Bu, zincirleme (chaining) şeklinde bir yapı oluşturarak kodun daha okunabilir hale gelmesine yardımcı olur.
  • Test senaryosunu temsil eden bir sınıf oluşturun ve bu sınıfta Fluent Design Pattern’ını kullanarak senaryoyu tasarlayın.

Standart hâle gelen oturum açma senaryosu üzerinden bu tasarım desenini anlamaya çalışalım.

public class LoginPage {
    WebDriver driver;
    By usernameInput = By.id("kullaniciAdi");
    By passwordInput = By.id("parola");
    By loginButton = By.id("btnGirisYap");

    public LoginPage(WebDriver driver) {
        this.driver = driver;
    }

    public LoginPage enterUsername(String username) {
        driver.findElement(usernameInput).clear();
        driver.findElement(usernameInput).sendKeys(username);
        return this;
    }

    public LoginPage enterPassword(String password) {
        driver.findElement(passwordInput).clear();
        driver.findElement(passwordInput).sendKeys(password);
        return this;
    }

    public HomePage clickLoginButton() {
        driver.findElement(loginButton).click();
        return new HomePage(driver);
    }

}
  • public LoginPage enterUsername(String username) { ... }: Kullanıcı adını girmek için kullanılan bir metottur. Metot, kullanıcı adı alanını temizler, ardından yeni kullanıcı adını girecek şekilde bu alanı bulur ve değeri set eder. Son olarak, this anahtar kelimesi ile mevcut sınıf örneğini döndürerek zincirleme bir yapı oluşturur.
  • public LoginPage enterPassword(String password) { ... }: Parolayı girmek için kullanılan bir metottur. Benzer şekilde çalışır ve zincirleme yapısını destekler.
  • public HomePage clickLoginButton() { ... }: Giriş butonuna tıklamak için kullanılan bir metottur. Giriş butonunu bulur, tıklar ve ardından yeni bir HomePage örneği oluşturarak döndürür.
public class LoginTest {
    WebDriver driver;
    LoginPage loginPage;

    @BeforeTest
    public void setUp() {
        driver = new ChromeDriver();
        driver.get("https://demo.yasinalbakir.net/");
        loginPage = new LoginPage(driver);
    }

    @Test
    public void loginTest() {
        loginPage.enterUsername("admin")
                .enterPassword("123456")
                .clickLoginButton();
    }

    @AfterTest
    public void tearDown() {
        driver.quit();
    }
}

loginPage.enterUsername("admin").enterPassword("123456").clickLoginButton(); Bu satır, Fluent tasarım desenini kullanarak LoginPage sınıfındaki giriş işlemlerini gerçekleştirir. Kullanıcı adı ve şifre girişi yapıldıktan sonra giriş butonuna tıklanır.


Strategy Design Pattern

Strategy tasarım deseni, bir algoritmanın farklı varyasyonlarını tanımlayan ve bu varyasyonları birbirinden bağımsız bir şekilde değiştirmemize olanak tanıyan davranışsal bir tasarım desenidir. Bu desen, bir algoritmanın kullanıldığı bağlamı değiştirmeden, farklı stratejileri uygulayabilmemize olanak sağlar.

Strategy tasarım deseninin diğer alanlarda da birçok benzer örneği bulunabilir. İşte birkaç örnek:

  • Ödeme İşlemleri: E-ticaret platformları gibi sistemlerde farklı ödeme stratejileri kullanılabilir. Kredi kartı, PayPal veya banka transferi gibi farklı ödeme yöntemleri, bir ödeme stratejisi olarak ele alınabilir ve kullanıcı tercihine bağlı olarak seçilebilir.
  • Taşıma ve Nakliye Stratejileri: Bir lojistik uygulamasında, farklı taşıma veya nakliye stratejileri kullanılabilir. Karayolu, denizyolu veya hava yolu gibi farklı nakliye stratejileri, taşıma işlemlerini etkileyebilir.
  • Sıralama Algoritmaları: Bir liste içindeki öğeleri sıralamak için farklı algoritmalar kullanılabilir. Örneğin, bir listenin sıralanması için bubble sort, quicksort veya mergesort gibi farklı stratejiler kullanılabilir. Bu durumda, bir sıralama stratejisi belirleyerek liste sıralama işlemini etkileyebilirsiniz.

Strateji tasarım desenini kullanmanın faydaları şunlardır:

  • Kodu daha modüler ve bakımı daha kolay hale getirir.
  • Yeni stratejiler eklemek veya mevcut stratejileri değiştirmek daha kolaydır.
  • Sistemin daha esnek ve uyarlanabilir olmasını sağlar.

Nasıl İmplemente Edilir?

  • Arayüz (Interface): Farklı stratejilerin uygulandığı sınıflar arasında bir arayüz belirlenir. Bu arayüz, strateji sınıflarının ortak bir davranışı paylaşmasını sağlar.
  • Strateji Sınıfları (Strategy Classes): Farklı stratejileri temsil eden sınıflardır. Bu sınıflar, belirlenen arayüzü uygular ve stratejiyi tanımlayan özel davranışlarını içerir.
  • Bağlam (Context): Strateji nesnelerini kullanacak olan sınıftır. Bağlam sınıfı, uygulanacak stratejiyi değiştirebilir ve bu sayede farklı davranışları elde edebilir.

Farklı ödeme yöntemlerini destekleyen bir e-ticaret sitesinin test otomasyonunu yapmak için bu tasarım desenini kullanabiliriz. Her bir ödeme yöntemi için bir strateji sınıfı oluşturabiliriz ve bağlam sınıfı, kullanılacak stratejiyi seçebiliriz, bu tasarım deseniyle birlikte ileride gelebilecek diğer ödeme yöntemlerini de (mesela bitcoin ile ödeme) yapımızı bozmadan implemente edebileceğiz, örneğimiz için oluşturduğum sınıf diyagramı aşağıdaki gibidir;

IPaymentStrategy adında soyut bir sınıf oluşturarak kodlamaya başlayacağız, her bir ödeme yöntemimiz için birer somut sınıf oluşturacağız ve IPaymentStrategy soyut sınıfında yer alan pay(Map<String, String> paymentDetails) methodunu implemente edeceğiz, paymentDetails parametresini geçmemizin sebebi ise ödeme yöntemine göre farklı parametreler gönderebilme ihtimalimizden dolayıdır. Yani kredi kartı kartıyla ödeme yaparken kredi kartı numarası, kart sahibinin adı soyadı gibi parametrelere ihtiyaç duyarken paypal ile ödemede bu parametreler gereksiz olacaktır. PagePayment adında somut bir sınıf oluşturacağız, ödeme için farklı yöntemleri kullanacağız. Son olarak test sınıfı ve methodlarımızı oluşturarak testlerimizi koşar duruma geleceğiz.

public interface IPaymentStrategy {
    void pay(Map<String, String> paymentDetails);
}
  • IPaymentStrategy arayüzü, ödeme stratejilerinin uygulamaları için bir şablondur.
  • pay adında bir metod içerir. Bu method, her bir ödeme stratejisinin kendi ödeme işlemini gerçekleştirmek üzere tasarlanmıştır.
  • Map<String, String> tipinde bir parametre alarak, ödeme işlemi için gerekli olan bilgilerin genel bir yapı içinde temsil edilmesini sağlar.
public class CreditCard implements IPaymentStrategy {
    private WebDriver driver;
    private By creditCardOption = By.id("credit card option id");
    private By creditCardNumber = By.id("card number element id");
    private By cardHolderFullName = By.id("card holder's full name element id");
    private By cvc = By.id("cvc number element id");


    @Override
    public void pay(Map<String, String> paymentDetails) {
        driver.findElement(creditCardOption).click();
        driver.findElement(creditCardNumber).sendKeys(paymentDetails.get("cardNumber"));
        driver.findElement(cardHolderFullName).sendKeys(paymentDetails.get("cardHolderName"));
        driver.findElement(cvc).sendKeys(paymentDetails.get("cvc"));
    }

}
public class PayPal implements IPaymentStrategy {
    private WebDriver driver;
    private By payPalOption = By.id("PayPal option id");
    private By email = By.id("PayPal email element id");

    @Override
    public void pay(Map<String, String> paymentDetails) {
        driver.findElement(payPalOption).click();
        driver.findElement(email).sendKeys(paymentDetails.get("email"));
    }

}
public class DigitalWallet implements IPaymentStrategy {

    private WebDriver driver;
    private By digitalWalletOption = By.id("Digital wallet option id");
    private By walletId = By.id("Digital wallet element id");

    @Override
    public void pay(Map<String, String> paymentDetails) {
        driver.findElement(digitalWalletOption).click();
        driver.findElement(walletId).sendKeys(paymentDetails.get("walletId"));
    }
}
  • CreditCard, DigitalWallet, ve PayPal sınıfları, IPaymentStrategy arayüzünü uygularlar.
  • Her bir sınıf, kendi ödeme stratejisi ile ilgili detayları içerir.
public class PaymentPage {
    private WebDriver driver;
    private IPaymentStrategy paymentStrategy;
    private By paymentButton = By.id("payment button id");


    public PaymentPage(WebDriver driver, IPaymentStrategy paymentStrategy) {
        this.driver = driver;
        this.paymentStrategy = paymentStrategy;
        driver.get("https://www.e-ticaret-example.com");
    }

    public void doPayment(Map<String, String> paymentDetails) {
        paymentStrategy.pay(paymentDetails);
        driver.findElement(paymentButton).click();
    }

}
  • PaymentPage sınıfının yapıcı metodunda, WebDriver ve IPaymentStrategy parametreleri alınır.
  • driver ve paymentStrategy alanları ilgili parametre değerleriyle atanır.
  • doPayment metodunda, paymentStrategy nesnesi üzerinden pay metodunu çağırarak belirli bir ödeme stratejisini gerçekleştirir.
  • Daha sonra, sayfadaki ödeme butonuna (paymentButton) tıklanarak ödeme işlemi tamamlanır.
  • Bu methodun parametre olarak bir Map<String, String> alması, ödeme bilgilerinin genel bir yapı içinde taşınmasına olanak tanır.
public class PaymentTest {
    WebDriver driver;
    PaymentPage paymentPage;

    @BeforeTest
    public void setup() {
        driver = new ChromeDriver();
    }

    @AfterTest
    public void teardown() {
        driver.quit();
    }

    @Test
    public void creditCardPaymentTest() {
        Map<String, String> creditCardDetails = new HashMap<>();
        creditCardDetails.put("cardNumber", "1234567890123456");
        creditCardDetails.put("cardHolderName", "John Doe");
        creditCardDetails.put("cvc", "123");

        IPaymentStrategy creditCardPayment = new CreditCard();
        paymentPage = new PaymentPage(driver, creditCardPayment);
        paymentPage.doPayment(creditCardDetails);

        //Assertion

    }

    @Test
    public void payPalPaymentTest() {
        Map<String, String> payPalCardDetails = new HashMap<>();
        payPalCardDetails.put("email", "abc@example.com");

        IPaymentStrategy payPalPayment = new PayPal();
        paymentPage = new PaymentPage(driver, payPalPayment);
        paymentPage.doPayment(payPalCardDetails);

        //Assertion

    }

    @Test
    public void digitalWalletPaymentTest() {
        Map<String, String> digitalWalletDetails = new HashMap<>();
        digitalWalletDetails.put("walletId", "5689566");

        IPaymentStrategy digitalWallet = new DigitalWallet();
        paymentPage = new PaymentPage(driver, digitalWallet);
        paymentPage.doPayment(digitalWalletDetails);

        //Assertion

    }

}
  • creditCardPaymentTest metodu, kredi kartı ödeme stratejisinin doğru çalıştığını doğrulayan bir test senaryosunu içerir.
  • Kredi kartı detayları bir Map içinde tanımlanır.
  • CreditCard sınıfından bir örnek oluşturulur ve bu örnek PaymentPage sınıfına geçirilir.
  • doPayment metodu kullanılarak ödeme işlemi gerçekleştirilir, diğer ödeme yöntemleri için de ayrı birer test methodu oluşturulmuştur.

Template Method Design Pattern

Bu desen, bir algoritmanın yapısını tanımlayan ancak bazı adımları alt sınıflara bırakan bir şablona dayanır. Bu sayede alt sınıflar, belirli adımları değiştirerek veya özelleştirerek kendi uygulamalarını yapabilirler. Bir sürecin (algoritmanın) iskeletini bir üst sınıfta belirlemek ve detayları alt sınıflara bırakmaktır. Bu sayede alt sınıflar, belirli adımları veya metotları kendi ihtiyaçlarına göre uyarlayabilirler.

Örneğin çeşitli yetkinliklere sahip çalışanlarımız olduğunu varsayalım, analist, test uzmanı, developer ve proje yöneticisi. Her bir çalışanımızın farklı görev tanımları bulunmaktadır. Fakat çalışanlarımızın günlük rutinleri bulunmaktadır, sabah kalkacaklar, kahvaltı yapacaklar, işe gelecekler, çalışacaklar ve evlerine gidecekler. Yani günlük rutinleri genellikle ortak fakat yaptıkları işler aldıkları sorumluluklar tamamen farklıdır. Template method tasarım deseni de tam olarak günlük rutin işlerimizlerin iskeletini üst sınıfta tanımlayarak alt sınıflarda bu rutin işlerin detayına inebiliriz. Detaylar alt sınıflarda gizlendiği için encapsulation ilkesini uygulamış oluruz, Ana iskelet soyut bir sınıftan oluştuğu için kodun genel tasarımını anlamayı kolaylaştırır ve sınıflar arasındaki ilişkileri düzenler.

Template Method Pattern’ın kullanım avantajları şunlar olabilir:

  • Kodun tekrar kullanımını sağlar.
  • Algoritmanın genel yapısını bir üst sınıfta belirleyerek soyutlamayı destekler.
  • Alt sınıfların belirli adımları uyarlaması için esneklik sağlar.
  • Algoritmanın merkezi bir noktadan kontrol edilmesini sağlar.

Template Method tasarım deseninin diğer alanlarda da birçok benzer örneği bulunabilir. İşte birkaç örnek:

  1. Kahve veya Çay Yapma Makineleri: Bir kahve veya çay yapma makinesi, belirli bir içeceği hazırlama sürecini temsil edebilir. Makinenin temel iskeleti içinde su ısıtma, demleme ve servis etme adımları bulunabilir. Bu adımlar soyut olarak tanımlanır ve alt sınıflar, bu adımları kendi içecek türlerine göre uyarlar.
  2. Oyun Geliştirme Framework’leri: Oyun geliştirme framework’leri, oyunlar arasında genel işlevselliği sağlamak için Template Method Pattern’ı kullanabilir. Örneğin, oyun öğelerinin güncellenmesi, çizilmesi ve işlenmesi gibi adımlar soyut bir oyun sınıfında belirlenebilir ve bu adımlar alt sınıflar tarafından özelleştirilebilir.
  3. Belge Dönüştürme Aracı: Bir belge dönüştürme aracı, belirli belge türlerini başka bir formata dönüştürme işlemini gerçekleştirebilir. İşlem adımları, Template Method Pattern kullanılarak soyutlanabilir ve alt sınıflar, belirli belge türlerine özgü dönüşüm adımlarını uyarlayabilir.
  4. E-ticaret Sistemi Sipariş İşleme: Bir e-ticaret sistemine ait bir sipariş işleme süreci, Template Method Pattern kullanarak soyutlanabilir. Siparişin oluşturulması, ödeme işlemi ve teslimat adımları soyut bir sınıf içinde tanımlanabilir ve alt sınıflar, bu adımları kendi ürün veya hizmet türlerine göre uyarlayabilir.
  5. Dinamik Rapor Oluşturma Aracı: Bir rapor oluşturma aracı, dinamik olarak farklı rapor türlerini üretebilir. Template Method Pattern, rapor oluşturma sürecini soyutlayabilir ve alt sınıflar, belirli rapor türlerine özgü adımları uyarlayabilir.

Nasıl İmplemente Edilir?

  1. Soyut Sınıf (Abstract Class): Algoritmanın iskeletini tanımlar. Bu sınıf içinde bir template method bulunur, bu metodun içinde belirli adımlar vardır. Bazı adımlar soyut olarak işaretlenmiştir (abstract), yani alt sınıflar tarafından implemente edilmelidir.
  2. Concrete Sınıflar (Concrete Classes): Soyut sınıftan türetilen ve soyut metodları implemente eden sınıflardır. Bu sınıflar, belirli adımları kendi ihtiyaçlarına göre uyarlar.
  3. Template Method: Algoritmanın iskeletini oluşturan ve belirli adımları içeren metodur. Bu metod genellikle final (değiştirilemez) olarak işaretlenir, böylece alt sınıflar bu template method’u değiştiremezler.
Template Method Tasarım Deseni Sınıf Diyagramı

Yukarıda sınıf diyagramını paylaştığım örneğimiz üzerinden bu tasarım desenini anlamaya çalışalım. Booking.com üzerinde otel rezervasyonu, uçak bileti, araba kiralama gibi farklı operasyonlar yer almaktadır, bu operasyonların tamamına hiç ayrıntıya girmeden en tepeden baktığımızda sırasıyla destinasyon ve tarih aralığı seçerek arama yapma, gelen sonuçları inceleyerek bize en uygun hizmeti seçme ve ödeme yapmak gibi temel süreçler yer almaktadır. Bu tasarım deseniyle temel operasyonları ortak bir method altında toplayacağız, alt sınıflarda da bu temel operasyonların altını dolduracağız. Her alt sınıf ilgili operasyonda yer alan locatorları ve methodları içerecek.

Önce, ortak iş akışını içeren bir template class (şablon sınıf) oluştalım. Bu sınıf, ana iş akışını tanımlayan bir metod içermelidir. Ancak, bazı adımları alt sınıflara bırakarak genelleme yapmalıdır.

public abstract class BookingTemplate {

    protected abstract void search();

    protected abstract void select();

    protected abstract void pay();

    public final void book() {
        search();
        search();
        pay();
    }
}

Şimdi, farklı operasyonları temsil eden alt sınıfları oluşturalım ve template sınıfını genişletelim. Her alt sınıf, ortak iş akışındaki soyut metodları kendi ihtiyaçlarına göre uygulamalıdır.

public class HotelBooking extends BookingTemplate {

    private WebDriver driver;
    private WebDriverWait wait;
    private String location;

    @FindBy(id = "destination-id")
    private WebElement destination;

    @FindBy(id = "search-btn-id")
    private WebElement searchBtn;

    @FindBy(id = "item-id")
    private WebElement item;

    @FindBy(id = "booknow-button-id")
    private WebElement bookNowBtn;

    @FindBy(id = "pay button-id")
    private WebElement payBtn;


    public HotelBooking(WebDriver driver, String location) {
        this.driver = driver;
        this.wait = new WebDriverWait(driver, Duration.ofSeconds(10));
        this.location = location;
    }

    @Override
    protected void search() {
        this.destination.sendKeys(location);
        this.searchBtn.click();
    }

    @Override
    protected void select() {
        this.wait.until((d) -> this.item.isDisplayed());
        this.bookNowBtn.click();
    }

    @Override
    protected void pay() {
        this.payBtn.click();
    }
}
public class FlightBooking extends BookingTemplate{
    private WebDriver driver;
    private WebDriverWait wait;
    private String location;

    @FindBy(id = "destination-id")
    private WebElement flightDestination;

    @FindBy(id = "search-btn-id")
    private WebElement searchBtn;

    @FindBy(id = "item-id")
    private WebElement item;

    @FindBy(id = "booknow-button-id")
    private WebElement bookNowBtn;

    @FindBy(id = "pay button-id")
    private WebElement payBtn;


    public FlightBooking(WebDriver driver, String location) {
        this.driver = driver;
        this.wait = new WebDriverWait(driver, Duration.ofSeconds(10));
        this.location = location;
    }

    @Override
    protected void search() {
        this.flightDestination.sendKeys(location);
        this.searchBtn.click();
    }

    @Override
    protected void select() {
        this.wait.until((d) -> this.item.isDisplayed());
        this.bookNowBtn.click();
    }

    @Override
    protected void pay() {
        this.payBtn.click();
    }
}

Şimdi, bu alt sınıfları kullanarak farklı operasyonların testlerini yazabiliriz.

public class BookingTest {
    WebDriver driver;

    @Test
    public void hotelBookingTest() {
        driver = new ChromeDriver();
        driver.get("www.booking.com");
        BookingTemplate hotelBooking = new HotelBooking(driver, "Amsterdam");
        hotelBooking.book();
    }

    @Test
    public void flightBookingTest() {
        driver = new ChromeDriver();
        driver.get("www.booking.com");
        BookingTemplate flightBooking = new FlightBooking(driver, "İstanbul");
        flightBooking.book();
    }
}

Bu tasarım deseni sayesinde Booking.com üzerindeki farklı operasyonları test etmek istediğinizde benzer temel iş akışlarını paylaştırabilir ve aynı zamanda her operasyonu özelleştirebilirsiniz.


Sonuç

Yazılım geliştirmenin dinamik ve sürekli evrim geçiren dünyasında, tasarım desenleri adeta bir rehber niteliği taşır. Bu yazı boyunca, test otomasyon projelerinde yaygın olarak kullanılan tasarım desenlerini keşfettik ve bu desenlerin sağladığı avantajlara odaklandık.

Ancak, her güçlü araç gibi tasarım desenleri de dikkatli şekilde ele alınmalıdır. Karmaşıklık, aşırı soyutlama, öğrenme eğrisi gibi dezavantajlar, bu desenlerin aşırı kullanımı veya yanlış uygulanması durumunda ortaya çıkabilir.

Tasarım desenleri, yazılım mimarisini güçlendirebilir, kodun bakımını kolaylaştırabilir ve proje ekibine genel bir yol haritası sunabilir. Ancak, her proje için doğru tasarım desenini seçmek, projenin gereksinimlerini anlamak ve ekibin ihtiyaçlarına uygun bir denge bulmak önemlidir.

Test otomasyon yolculuğunuzda tasarım desenlerini etkili bir şekilde kullanmanız dileğiyle.

Sağlıklı günler dilerim.