Фабричный метод в автоматизации тестирования

Перевод статьи «Understanding Design Patterns in Test Automation: Factory Method Pattern».

Итак, вы прочитали книгу или прошли курс по основам Selenium и знаете, как создать простой скрипт, который запускает сайт, вводит определенные данные и проверяет результат. Поздравляем! Теперь вы понимаете, на что способна автоматизация тестирования и как вы можете ее использовать для выполнения рутинных и громоздких ручных тестов, освобождая время для исследовательского тестирования.

Однако, когда вы пишете уже третий или двадцатый тест, вы начинаете понимать, что можно избежать дублирования кода, и возникает вопрос, как это лучше сделать. Более того, вы написали скрипт Selenium для запуска тестов в браузере Chrome. Но что если перед вами теперь стоит задача запустить эти тесты в Firefox? Быстрое решение – поместить код внутри блока if else, чтобы проверять, какой браузер запустить. Вероятно, ваш тест будет выглядеть примерно так, как показано ниже.

Подпишитесь на наш ТЕЛЕГРАМ КАНАЛ ПО АВТОМАТИЗАЦИИ ТЕСТИРОВАНИЯ
package factorypatterntest;

import io.github.bonigarcia.wdm.WebDriverManager;
import io.github.bonigarcia.wdm.config.DriverManagerType;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.firefox.FirefoxOptions;

public class RudimentaryTest {

    @ParameterizedTest()
    @ValueSource(strings = {"chrome"})
    void testBrowserLaunch(String browser) {

        WebDriver driver = null;
        if ("chrome".equalsIgnoreCase(browser)) {
            WebDriverManager.getInstance(DriverManagerType.CHROME).setup();
            ChromeOptions options = new ChromeOptions();
            driver = new ChromeDriver(options);
        } else if ("firefox".equalsIgnoreCase(browser)) {
            WebDriverManager.getInstance(DriverManagerType.FIREFOX).setup();
            FirefoxOptions options = new FirefoxOptions();
            driver = new FirefoxDriver(options);
        }

        driver.get("https://www.phptravels.net/home");
        Assertions.assertEquals("PHPTRAVELS | Travel Technology Partner", driver.getTitle());
        driver.quit();
    }
}

Надеюсь, вы заметили проблему, которая может возникнуть с таким кодом. Если нам, например, понадобится поддержка другого браузера, придется добавлять для него еще одно условие if. Это нарушает принцип открытости-закрытости (Open-Closed Principle, OCP) SOLID, потому что теперь ваш класс открыт для модификации. Вдобавок, избыточное использование if else всегда выглядят слишком громоздко. Можно ли сделать код более более лаконичным? Конечно, почему бы и нет. Попробуем воспользоваться набором паттернов проектирования, которые рекомендует «Банда четырёх».

Проблема здесь заключается в том, что у нас есть набор операторов, которые могут создавать различные типы экземпляров веб-драйвера, и в зависимости от условия нам необходимо запускать один из них в любой момент времени. Для решения этой проблемы мы, вероятно, можем создать фабрику драйверов, которая принимает тип браузера в качестве входных данных и возвращает соответствующий экземпляр веб-драйвера. Вуаля! Мы разработали шаблон проектирования, и это ничто иное, как паттерн Фабричного метода (Factory Method Pattern).

Давайте разберем паттерн Фабрики немного поподробнее. Согласно определению: “Паттерн фабричного метода определяет общий интерфейс для создания объектов, но позволяет подклассам выбрать класс создаваемого объекта. Метод фабрики позволяет классу делегировать создание объектов подклассам”.

Термин интерфейс не обязательно должен быть интерфейсом в Java. Это может быть рассмотрено как правила взаимодействия (контракт) в виде интерфейса или абстрактного класса. Разработчику не обязательно знать, какой продукт должен быть создан. Класс, отвечающий за реализацию контракта, должен принимать решение во время выполнения программы на основе входных данных. Класс-создатель пишется без знания информации о конкретных продуктах, которые будут созданы. Давайте рассмотрим на примере.

Далее показан пример интерфейса, а также конкретный класс, реализующий этот интерфейс по шаблону Фабричного метода.

package factorypattern.factory;

import org.openqa.selenium.WebDriver;

public interface BrowserFactory {
    WebDriver createDriver(String browser);
}

Объявление интерфейса для Фабрики

package factorypattern.factory;

import io.github.bonigarcia.wdm.WebDriverManager;
import io.github.bonigarcia.wdm.config.DriverManagerType;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.edge.EdgeDriver;
import org.openqa.selenium.edge.EdgeOptions;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.firefox.FirefoxOptions;
import org.openqa.selenium.safari.SafariDriver;
import org.openqa.selenium.safari.SafariOptions;

import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;

public class BrowserFactoryImpl implements BrowserFactory {

    private Supplier<WebDriver> chromeDriver = () -> {
        WebDriverManager.getInstance(DriverManagerType.CHROME).setup();
        ChromeOptions options = new ChromeOptions();
        WebDriver driver = new ChromeDriver(options);
        return driver;
    };

    private Supplier<WebDriver> firefoxDriver = () -> {
        WebDriverManager.getInstance(DriverManagerType.FIREFOX).setup();
        FirefoxOptions options = new FirefoxOptions();
        WebDriver driver = new FirefoxDriver(options);
        return driver;
    };

    private Supplier<WebDriver> edgeDriver = () -> {
        WebDriverManager.getInstance(DriverManagerType.EDGE).setup();
        EdgeOptions options = new EdgeOptions();
        WebDriver driver = new EdgeDriver(options);
        return driver;
    };

    private Supplier<WebDriver> safariDriver = () -> {
        WebDriverManager.getInstance(DriverManagerType.SAFARI).setup();
        SafariOptions options = new SafariOptions();
        WebDriver driver = new SafariDriver(options);
        return driver;
    };

    private Map<String, Supplier<WebDriver>> driverMap = new HashMap<>();

    public BrowserFactoryImpl() {
        driverMap.put("firefox", firefoxDriver);
        driverMap.put("chrome", chromeDriver);
        driverMap.put("edge", edgeDriver);
        driverMap.put("safari", safariDriver);
    }

    @Override
    public WebDriver createDriver(String browser) {
        return driverMap.getOrDefault(browser.toLowerCase(), chromeDriver).get();
    }

}

Фабричный класс, который реализует логику создания driver на основе входных данных

В представленном примере у нас есть интерфейс BrowserFactory, который определяет метод createDriver, а BrowserFactoryImpl реализует этот интерфейс. BrowserFactoryImpl принимает на входе параметр и в зависимости от него возвращает подходящий экземпляр driver.

Вот тест, который использует паттерн Фабричного метода для вызова соответствующей сессии webdriver:

package factorypatterntest;


import factorypattern.factory.BrowserFactory;
import factorypattern.factory.BrowserFactoryImpl;
import org.junit.jupiter.api.*;
import org.openqa.selenium.WebDriver;

public class FactoryPatternTest {

    WebDriver driver;
    BrowserFactory factory;

    @BeforeEach
    @DisplayName("создание экземпляра Фабрики перед каждым тестом")
    public void beforEach() {
        factory = new BrowserFactoryImpl();
    }

    @Test
    @DisplayName("запуск теста в chrome")
    public void runTestOnChrome() {
        driver = factory.createDriver("chrome");

        driver.get("https://www.phptravels.net/home");
        Assertions.assertEquals("PHPTRAVELS | Travel Technology Partner", driver.getTitle());
    }

    @Test
    @DisplayName("запуск теста в firefox")
    public void runTestOnFirefox() {
        driver = factory.createDriver("firefox");

        driver.get("https://www.phptravels.net/home");
        Assertions.assertEquals("PHPTRAVELS | Travel Technology Partner", driver.getTitle());

    }

    @AfterEach
    @DisplayName("закрытие экземпляра webdriver instance после каждого теста")
    public void afterEach() {
        driver.quit();
    }
}

Тест, который использует шаблон Фабричного метода для вызова соответствующей сессии веб-драйвера

Думаю, вы заметили здесь два необычных момента.

  • Мы создали объект типа BrowserFactoryImpl, хотя могли бы создать конкретный BrowserFactory и сделать его статическим или классом-синглтоном (Singletone), возвращая экземпляр webdriver в зависимости от входного аргумента – типа браузера. Это 100% сработает, и вы можете встретить это во многих фреймворках. Однако, с точки зрения строгого определения, это не Фабричный паттерн. Кроме того, в некоторых случаях вам может понадобиться другая реализация Фабрики.
  • Мы могли бы использовать блок if..else или switch case в методе createDriver BrowserFactoryImpl для сравнивания входного аргумента с предопределенным набором имен браузеров и возврата подходящего экземпляра. Однако по мере добавления новых браузеров содержимое внутри блока будет расти и выглядеть громоздко, утрачивая читабельность. Поэтому мы воспользовались интерфейсом Supplier, чтобы определить webdriver для каждого возможного браузера и добавить его в map. А createDriver просто должен вернуть значение из map, соответствующее ключу.

Эти вещи делают наш код таким аккуратным и управляемым. Если нам понадобится добавить другой браузер, например, Brave или IE, мы можем просто добавить его в map и быть уверенными, что на выходе получим экземпляр.

Смотрите также: “Объектная модель страницы (POM) и фабрика страниц в Selenium”

Заключение

Паттерн фабричного метода предоставляет элегантное решение для создания объектов, избегая привязки к конкретным классам. Этот подход способствует увеличению гибкости кода и его поддержке.

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *