Оглавление

Введение

Привет! В данной статье мы рассмотрим какие есть паттерны проектирования и как их можно использовать в написании автотестах. Данная статья будет полезна тем кто только начинает свой путь в автоматизации или повторяет материал перед собеседованием.

Когда мы пишем автотесты, кажется, что главное — это покрыть функционал. Но спустя несколько месяцев код обрастает костылями, тесты начинают падать «ни с того ни с сего», а новые люди в команде ломают голову, как вообще устроен проект.

Здесь на помощь приходят паттерны проектирования. Это проверенные временем решения типовых проблем, которые делают тестовый код поддерживаемым, гибким и понятным.

Классификация паттернов

Прежде чем делить паттерны на группы, важно понять, что это вообще такое.

Классификация паттернов проектирования в автотестах
Классификация паттернов проектирования в автотестах

Паттерн проектирования — это повторяемое архитектурное решение типовой задачи в программировании. Он не даёт готовый код, а описывает идею, которую можно адаптировать под конкретный проект.

Пример из жизни: если вы строите дом, то «однокомнатная квартира» или «таунхаус» — это паттерны планировки. Каждый архитектор может реализовать их по-своему, но принцип остаётся.

Основные группы паттернов

  1. Порождающие
    Эти паттерны отвечают за создание объектов. Они помогают избавиться от «new везде» и делают процесс гибче.

    В автотестах порождающие паттерны спасают от копипаста при создании тестовых пользователей, заказов, токенов и т.п.

  2. Структурные
    Эти паттерны помогают организовать связи между объектами и классами, чтобы код был чище и легче поддерживался.

    В автотестах структурные паттерны позволяют сделать код «как Lego»: из маленьких частей строится целая архитектура.

  3. Поведенческие
    Эти паттерны описывают алгоритмы взаимодействия объектов. Они делают систему гибкой и расширяемой.

    В автотестах поведенческие паттерны помогают описывать сценарии ближе к бизнес-логике и избегать монолитных «тестов-монстров».

Применение паттернов в автотестах

Когда мы строим проект автотестов, мы фактически создаём программную систему, а не просто набор скриптов. У такой системы есть архитектура, зависимости, слои и логика — всё как в настоящем приложении.
И чем больше тестов, тем быстрее всё превращается в хаос, если не придерживаться архитектурных принципов.

Здесь и приходят на помощь паттерны проектирования.

Паттерны помогают:

  • Разделять ответственность между классами (Single Responsibility).

  • Строить гибкую архитектуру, где легко добавлять новые тесты и сценарии.

  • Избегать дублирования и упрощать рефакторинг.

  • Делать тесты понятными для других членов команды.

  • Воспринимать тестовую систему как живой проект, а не как набор скриптов.

В мире тестирования паттерны применяются на всех уровнях:

Уровень

Примеры паттернов

Описание

UI-тесты

Page Object, Screenplay, Facade

Упрощают работу со страницами и действиями пользователя

API-тесты

Builder, Factory, Strategy

Помогают гибко формировать запросы и данные

Infrastructure

Singleton, Proxy, Adapter

Управляют ресурсами и внешними зависимостями

Тестовая логика

Template Method, State, Chain of Responsibility

Описывают последовательность шагов и поведения

Порождающие паттерны

Factory

Factory
Factory

Описание:
Фабричный метод (Factory Method) — это порождающий паттерн, который решает проблему создания объектов через единый интерфейс, не привязываясь к конкретным классам. Идея в том, что код, который использует объект, не знает и не зависит от того, какой конкретно объект создается.

Это полезно, когда:

  • Требуется создать объекты с одинаковым интерфейсом, но разной реализацией.

  • Нужно легко менять конкретные реализации без изменения клиентского кода.

  • Хотим централизовать контроль за созданием объектов.

В автотестах Factory особенно удобно использовать для:

  • Создания тестовых данных (пользователи, заказы, токены).

  • Инициализации страниц (Page Object) или клиентов API.

  • Настройки окружений тестирования с разными конфигурациями.

Пример на Java (создание тестовых пользователей):

// Интерфейс пользователя
public interface User {
    String getName();
    String getRole();
}

// Конкретные реализации
public class AdminUser implements User {
    public String getName() { return "Admin"; }
    public String getRole() { return "Administrator"; }
}

public class GuestUser implements User {
    public String getName() { return "Guest"; }
    public String getRole() { return "Visitor"; }
}

// Фабрика пользователей
public class UserFactory {
    public static User createUser(String type) {
        switch (type.toLowerCase()) {
            case "admin": return new AdminUser();
            case "guest": return new GuestUser();
            default: throw new IllegalArgumentException("Unknown user type");
        }
    }
}

// Использование в тесте
public class UserTest {
    public static void main(String[] args) {
        User admin = UserFactory.createUser("admin");
        User guest = UserFactory.createUser("guest");

        System.out.println(admin.getName() + " - " + admin.getRole());
        System.out.println(guest.getName() + " - " + guest.getRole());
    }
}

Builder

Builder
Builder

Описание:
Builder — это порождающий паттерн, который позволяет поэтапно создавать сложные объекты, отделяя процесс конструирования от конечного представления.
Идея в том, что один и тот же процесс построения можно использовать для создания разных вариаций объекта.

Паттерн особенно полезен, когда:

  • Объект имеет много параметров, часть из которых опциональна.

  • Хотим избегать длинных конструкторов с множеством аргументов.

  • Необходимо читаемое и безопасное создание объектов в тестах.

Применение в автотестах:

  • Генерация тестовых DTO для API-запросов.

  • Создание сложных пользователей или заказов с разными атрибутами.

  • Формирование JSON или XML payload для тестов.

Пример на Java (создание тестового пользователя через Builder):

// Класс пользователя
public class User {
    private final String name;
    private final String role;
    private final int age;
    private final String email;

    private User(UserBuilder builder) {
        this.name = builder.name;
        this.role = builder.role;
        this.age = builder.age;
        this.email = builder.email;
    }

    public static class UserBuilder {
        private String name;
        private String role;
        private int age;
        private String email;

        public UserBuilder setName(String name) {
            this.name = name;
            return this;
        }

        public UserBuilder setRole(String role) {
            this.role = role;
            return this;
        }

        public UserBuilder setAge(int age) {
            this.age = age;
            return this;
        }

        public UserBuilder setEmail(String email) {
            this.email = email;
            return this;
        }

        public User build() {
            return new User(this);
        }
    }

    @Override
    public String toString() {
        return name + " (" + role + "), age: " + age + ", email: " + email;
    }
}

// Использование в тесте
public class UserTest {
    public static void main(String[] args) {
        User admin = new User.UserBuilder()
                        .setName("Alice")
                        .setRole("Administrator")
                        .setAge(30)
                        .setEmail("alice@example.com")
                        .build();

        User guest = new User.UserBuilder()
                        .setName("Bob")
                        .setRole("Visitor")
                        .build(); // некоторые поля можно пропустить

        System.out.println(admin);
        System.out.println(guest);
    }
}

Singleton

Описание:
Паттерн Singleton гарантирует, что у класса существует только один экземпляр, и предоставляет глобальную точку доступа к нему.
Он часто используется, когда необходимо централизованно управлять общими ресурсами, которые не должны создаваться повторно.

Ключевые особенности:

  • Контролирует количество экземпляров класса (всегда один).

  • Предоставляет статический метод доступа (getInstance()), который возвращает этот единственный экземпляр.

  • Часто используется вместе с ленивой инициализацией (объект создаётся только при первом обращении).

Применение в автотестах:

  • Управление экземпляром WebDriver в UI-тестах.

  • Хранение общих настроек окружения (URL, credentials, конфигурации).

  • Использование общего логгера или клиента API.

  • Работа с единственным подключением к БД в интеграционных тестах.

Пример на Java (Singleton для WebDriver):

public class DriverManager {
    private static DriverManager instance;
    private WebDriver driver;

    private DriverManager() {
        // Инициализация драйвера через WebDriverManager
        WebDriverManager.chromedriver().setup();
        driver = new ChromeDriver();
    }

    public static synchronized DriverManager getInstance() {
        if (instance == null) {
            instance = new DriverManager();
        }
        return instance;
    }

    public WebDriver getDriver() {
        return driver;
    }

    public void quitDriver() {
        if (driver != null) {
            driver.quit();
            driver = null;
            instance = null;
        }
    }
}

Использование в тесте:

public class LoginTest {
    @Test
    public void loginUser() {
        WebDriver driver = DriverManager.getInstance().getDriver();
        driver.get("https://example.com/login");
        // ... тестовые шаги
    }
}

Prototype

Описание:
Паттерн Prototype (Прототип) создаёт новые объекты путём копирования существующих, а не через вызов конструктора.
Он особенно полезен, когда создание объекта — это дорогая операция, или когда нужно быстро получить множество похожих экземпляров с небольшими изменениями.

Ключевые особенности:

  • Основан на методе clone() или пользовательском копировании.

  • Позволяет создавать копии без знания внутренней структуры класса.

  • Удобен, когда объекты имеют сложную иерархию или множество параметров.

Применение в автотестах:

  • Клонирование шаблонных DTO или JSON-запросов с разными значениями.

  • Повторное использование типовых тестовых пользователей, заказов, платежей и т.п.

  • Ускорение подготовки тестовых данных, минимизация копипаста.

Пример на Java (клонирование объекта пользователя):

public class User implements Cloneable {
    private String name;
    private String role;
    private String email;

    public User(String name, String role, String email) {
        this.name = name;
        this.role = role;
        this.email = email;
    }

    @Override
    public User clone() {
        try {
            return (User) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }

    @Override
    public String toString() {
        return name + " (" + role + ") — " + email;
    }
}

// Использование
public class PrototypeExample {
    public static void main(String[] args) {
        User baseUser = new User("Admin", "ADMIN", "admin@test.com");
        User testUser = baseUser.clone();
        testUser = new User("Tester", "USER", "qa@test.com");

        System.out.println(baseUser);
        System.out.println(testUser);
    }
}

Abstract Factory

Abstract Factory
Abstract Factory

Описание:
Паттерн Abstract Factory (Абстрактная фабрика) предоставляет интерфейс для создания семейств связанных объектов без указания их конкретных классов.
Он помогает изолировать код от деталей реализации и делает возможным лёгкий выбор нужного набора объектов в зависимости от контекста.

Ключевая идея:
Создать «фабрику фабрик» — класс, который знает, какие конкретные фабрики нужно использовать для создания нужных объектов.

Применение в автотестах:

  • Когда тесты должны работать с разными платформами — например, Web и Mobile.

  • При создании унифицированного интерфейса для Page Object или API-клиентов под разные окружения.

  • Для генерации объектов конфигурации, зависящих от типа тестируемой системы.

Пример на Java (выбор фабрики для разных платформ):

// 1. Общий интерфейс страницы логина
public interface LoginPage {
    void login(String username, String password);
}

// 2. Реализации для разных платформ
public class WebLoginPage implements LoginPage {
    public void login(String username, String password) {
        System.out.println("Web login with user: " + username);
    }
}

public class MobileLoginPage implements LoginPage {
    public void login(String username, String password) {
        System.out.println("Mobile login with user: " + username);
    }
}

// 3. Абстрактная фабрика
public interface PageFactory {
    LoginPage createLoginPage();
}

// 4. Конкретные фабрики
public class WebPageFactory implements PageFactory {
    public LoginPage createLoginPage() {
        return new WebLoginPage();
    }
}

public class MobilePageFactory implements PageFactory {
    public LoginPage createLoginPage() {
        return new MobileLoginPage();
    }
}

// 5. Использование
public class AbstractFactoryExample {
    public static void main(String[] args) {
        String platform = "mobile"; // подставляется из конфигурации
        PageFactory factory = platform.equals("web")
                ? new WebPageFactory()
                : new MobilePageFactory();

        LoginPage loginPage = factory.createLoginPage();
        loginPage.login("test_user", "password123");
    }
}

Структурные паттерны

Facade

Facade
Facade

Описание:
Паттерн Facade (Фасад) предоставляет упрощённый интерфейс к сложной системе классов или модулей.
Он скрывает внутреннюю реализацию и объединяет часто используемые операции в единый, понятный API.

Ключевая идея:
Создать один класс-обёртку, который инкапсулирует детали взаимодействия с разными частями системы, предоставляя тестам лаконичные и читаемые вызовы.

Применение в автотестах:

  • Объединение нескольких шагов (например, авторизация, создание сущности, проверка результата) в один метод.

  • Инкапсуляция сложных взаимодействий с UI, API и БД в одном месте.

  • Упрощение повторного использования сценариев.

  • Формирование «DSL» (Domain Specific Language) для тестов — чтобы они выглядели как бизнес-сценарии.

Пример на Java (Фасад для входа в систему):

// Класс для работы с UI
public class LoginUI {
    public void openLoginPage() {
        System.out.println("Открываем страницу логина");
    }

    public void enterCredentials(String user, String password) {
        System.out.println("Вводим данные: " + user);
    }

    public void submit() {
        System.out.println("Нажимаем кнопку Войти");
    }
}

// Класс для работы с API
public class LoginAPI {
    public String getAuthToken(String user, String password) {
        System.out.println("Получаем токен по API для " + user);
        return "token123";
    }
}

// Фасад
public class AuthFacade {
    private final LoginUI ui = new LoginUI();
    private final LoginAPI api = new LoginAPI();

    public void loginUser(String user, String password) {
        ui.openLoginPage();
        ui.enterCredentials(user, password);
        ui.submit();
        String token = api.getAuthToken(user, password);
        System.out.println("Авторизация завершена, токен: " + token);
    }
}

// Использование в тесте
public class FacadeExample {
    public static void main(String[] args) {
        AuthFacade auth = new AuthFacade();
        auth.loginUser("test_user", "password123");
    }
}

Decorator

Описание:
Паттерн Decorator (Декоратор) позволяет динамически добавлять новое поведение объекту, не изменяя его исходный код.
Вместо наследования используется композиция — объект «оборачивается» в другой объект, который расширяет его поведение.

Ключевая идея:
Создать обёртку (декоратор) вокруг существующего объекта, которая добавляет новую функциональность — логирование, метрики, обработку ошибок и т.п.

Применение в автотестах:

  • Добавление логирования или метрик к существующим шагам без изменения их кода.

  • Подсчёт времени выполнения тестов или запросов.

  • Динамическая модификация API-запросов (например, добавление токенов, хедеров).

  • Расширение функционала Page Object или клиентов API на уровне инфраструктуры.

Пример на Java (логирование через декоратор):

// Базовый интерфейс
public interface ApiClient {
    void sendRequest(String endpoint);
}

// Конкретная реализация
public class DefaultApiClient implements ApiClient {
    @Override
    public void sendRequest(String endpoint) {
        System.out.println("Отправляем запрос: " + endpoint);
    }
}

// Декоратор
public class LoggingApiClientDecorator implements ApiClient {
    private final ApiClient client;

    public LoggingApiClientDecorator(ApiClient client) {
        this.client = client;
    }

    @Override
    public void sendRequest(String endpoint) {
        System.out.println("[LOG] Старт запроса: " + endpoint);
        long start = System.currentTimeMillis();

        client.sendRequest(endpoint);

        long duration = System.currentTimeMillis() - start;
        System.out.println("[LOG] Запрос выполнен за " + duration + " мс");
    }
}

// Использование в тесте
public class DecoratorExample {
    public static void main(String[] args) {
        ApiClient client = new LoggingApiClientDecorator(new DefaultApiClient());
        client.sendRequest("/api/v1/users");
    }
}

Adapter

Описание:
Паттерн Adapter (Адаптер) преобразует интерфейс одного класса к другому, ожидаемому клиентом.
Он служит «переходником» между несовместимыми системами, позволяя использовать их совместно без изменения исходного кода.

Ключевая идея:
Создать промежуточный слой, который преобразует вызовы одного интерфейса в другой, сохраняя при этом изоляцию модулей.

Применение в автотестах:

  • Унификация работы с разными API (REST, GraphQL, gRPC).

  • Поддержка нескольких драйверов (например, Selenium и Appium).

  • Преобразование разных форматов данных — JSON ↔ XML, DTO ↔ Entity.

  • Использование «старого» кода в новом тестовом фреймворке без переписывания.

Пример на Java (адаптация разных API-клиентов):

// Целевой интерфейс — то, что ожидает тест
public interface UserService {
    User getUserById(String id);
}

// Существующий класс с несовместимым интерфейсом
public class LegacyUserApi {
    public String fetchUser(String userId) {
        return "{ \"name\": \"John Doe\" }"; // возвращает JSON
    }
}

// Адаптер
public class LegacyUserApiAdapter implements UserService {
    private final LegacyUserApi legacyApi;

    public LegacyUserApiAdapter(LegacyUserApi legacyApi) {
        this.legacyApi = legacyApi;
    }

    @Override
    public User getUserById(String id) {
        String json = legacyApi.fetchUser(id);
        // Конвертация JSON → объект User
        return new Gson().fromJson(json, User.class);
    }
}

// Пример использования в тесте
public class AdapterExample {
    public static void main(String[] args) {
        UserService userService = new LegacyUserApiAdapter(new LegacyUserApi());
        User user = userService.getUserById("42");
        System.out.println(user.getName());
    }
}

Composite

Описание:
Паттерн Composite (Компоновщик) позволяет объединять объекты в древовидные структуры и работать с ними как с единым целым.
Он делает взаимодействие с одиночными объектами и их группами одинаковым с точки зрения клиента.

Ключевая идея:
Создать общий интерфейс для простых и составных объектов, чтобы тест или бизнес-логика не зависели от внутренней структуры элементов.

Применение в автотестах:

  • Моделирование сложных UI-компонентов (списки, таблицы, формы).

  • Представление иерархий страниц или элементов.

  • Построение деревьев тестовых шагов или сценариев.

  • Упрощение обработки вложенных структур данных (JSON, XML).

Пример на Java (иерархия элементов UI):

// Общий интерфейс для элементов
interface UIComponent {
    void click();
}

// Простой элемент
class Button implements UIComponent {
    private final String name;
    public Button(String name) { this.name = name; }

    @Override
    public void click() {
        System.out.println("Нажатие на кнопку: " + name);
    }
}

// Составной элемент — может содержать другие
class Form implements UIComponent {
    private final List<UIComponent> components = new ArrayList<>();

    public void add(UIComponent component) {
        components.add(component);
    }

    @Override
    public void click() {
        for (UIComponent component : components) {
            component.click();
        }
    }
}

// Пример использования в тесте
public class CompositeExample {
    public static void main(String[] args) {
        Button loginBtn = new Button("Login");
        Button registerBtn = new Button("Register");

        Form authForm = new Form();
        authForm.add(loginBtn);
        authForm.add(registerBtn);

        authForm.click(); // кликает по всем кнопкам в форме
    }
}

Proxy

Описание:
Паттерн Proxy (Заместитель) предоставляет объект, который выступает «прослойкой» между клиентом и реальным объектом.
Он контролирует доступ, добавляет дополнительное поведение (например, логирование, кеширование, авторизацию) — без изменения кода самого объекта.

Ключевая идея:
Создать класс-заместитель, реализующий тот же интерфейс, что и оригинал, и перехватывать вызовы к нему.

Применение в автотестах:

  • Подмена реальных API через WireMock, MockServer, LocalStack.

  • Логирование и анализ сетевых запросов.

  • Кэширование ответов для ускорения повторных тестов.

  • Имитация поведения нестабильных внешних сервисов.

  • Создание «тестового шлюза» между тестами и реальной системой.

Пример на Java (логирующий прокси):

// Интерфейс сервиса
interface ApiClient {
    String getUser(String id);
}

// Реальный клиент
class RealApiClient implements ApiClient {
    @Override
    public String getUser(String id) {
        // эмуляция вызова реального API
        System.out.println("Выполняется запрос к API: /users/" + id);
        return "{ \"id\": \"" + id + "\", \"name\": \"Test User\" }";
    }
}

// Прокси с логированием
class LoggingProxy implements ApiClient {
    private final ApiClient realClient;

    public LoggingProxy(ApiClient realClient) {
        this.realClient = realClient;
    }

    @Override
    public String getUser(String id) {
        System.out.println("[LOG] Запрос пользователя: " + id);
        String response = realClient.getUser(id);
        System.out.println("[LOG] Ответ: " + response);
        return response;
    }
}

// Пример использования
public class ProxyExample {
    public static void main(String[] args) {
        ApiClient client = new LoggingProxy(new RealApiClient());
        client.getUser("123");
    }
}

Поведенческие паттерны

Strategy

Strategy
Strategy

Описание:
Паттерн Strategy (Стратегия) определяет семейство алгоритмов, инкапсулирует каждый из них и делает их взаимозаменяемыми.
Это позволяет изменять поведение программы во время выполнения, не изменяя клиентский код.

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

Применение в автотестах:

  • Используется для выбора способа авторизации (по паролю, токену, через API).

  • Позволяет менять стратегию валидации данных в зависимости от окружения (dev, stage, prod).

  • Упрощает тестирование различных сценариев логики без дублирования кода.

  • Помогает гибко конфигурировать тестовые шаги (например, разный способ получения данных — из API, UI или БД).

Пример на Java (разные стратегии авторизации):

// Общий интерфейс стратегии
interface AuthStrategy {
    void authenticate();
}

// Авторизация по паролю
class PasswordAuth implements AuthStrategy {
    @Override
    public void authenticate() {
        System.out.println("Авторизация по логину и паролю");
    }
}

// Авторизация по токену
class TokenAuth implements AuthStrategy {
    @Override
    public void authenticate() {
        System.out.println("Авторизация с помощью токена");
    }
}

// Авторизация через API
class ApiAuth implements AuthStrategy {
    @Override
    public void authenticate() {
        System.out.println("Авторизация через API-запрос");
    }
}

// Контекст, использующий стратегию
class AuthContext {
    private AuthStrategy strategy;

    public void setStrategy(AuthStrategy strategy) {
        this.strategy = strategy;
    }

    public void execute() {
        strategy.authenticate();
    }
}

// Пример использования
public class StrategyExample {
    public static void main(String[] args) {
        AuthContext context = new AuthContext();

        context.setStrategy(new PasswordAuth());
        context.execute();

        context.setStrategy(new TokenAuth());
        context.execute();

        context.setStrategy(new ApiAuth());
        context.execute();
    }
}

Observer

Описание:
Паттерн Observer (Наблюдатель) устанавливает зависимость «один ко многим» между объектами:
когда состояние одного объекта (издателя) изменяется — все зависимые объекты (подписчики) получают уведомление и реагируют соответствующим образом.

Ключевая идея:
Разорвать жёсткую связь между объектами, чтобы издатель не знал деталей о своих подписчиках — только то, что они реализуют общий интерфейс наблюдателя.

Применение в автотестах:

  • Подписка на события из Kafka, RabbitMQ или WebSocket для валидации, что нужное событие пришло.

  • Реакция на изменения состояния UI (например, ожидание появления элемента после клика).

  • Логирование событий в тестах (например, слушатель, фиксирующий все REST-запросы).

  • Слежение за тестовыми метриками — время выполнения, количество ошибок и т.п.

Пример на Java (подписка на события):

import java.util.*;

// Интерфейс наблюдателя
interface Observer {
    void update(String event);
}

// Издатель
class EventManager {
    private final List<Observer> observers = new ArrayList<>();

    public void subscribe(Observer observer) {
        observers.add(observer);
    }

    public void unsubscribe(Observer observer) {
        observers.remove(observer);
    }

    public void notifyObservers(String event) {
        for (Observer o : observers) {
            o.update(event);
        }
    }
}

// Конкретные наблюдатели
class LogListener implements Observer {
    @Override
    public void update(String event) {
        System.out.println("Логгер: получено событие — " + event);
    }
}

class AlertListener implements Observer {
    @Override
    public void update(String event) {
        System.out.println("Система уведомлений: событие — " + event);
    }
}

// Пример использования
public class ObserverExample {
    public static void main(String[] args) {
        EventManager manager = new EventManager();

        manager.subscribe(new LogListener());
        manager.subscribe(new AlertListener());

        manager.notifyObservers("Kafka topic updated");
        manager.notifyObservers("User logged in");
    }
}

Screenplay / Command

Описание:
Паттерн Command (Команда) инкапсулирует действие (операцию) в отдельный объект, отделяя то, что делается, от того, кто это делает.
На основе него построена модель Screenplay, популярная в тестовой автоматизации: каждое действие пользователя оформляется как команда (Action), которую выполняет актор (Actor).

Это позволяет описывать тесты в стиле:

«Пользователь Андрей открывает страницу, вводит логин, нажимает кнопку и видит сообщение об успехе».

Такой подход делает тесты читаемыми как сценарии и легко расширяемыми.

Применение в автотестах:

  • Каждый шаг UI или API-теста оформляется как объект-команда (например, Login, SearchProduct, SubmitOrder).

  • Повторно используемые действия объединяются в Tasks.

  • Тесты становятся декларативными и понятными даже нетехническим специалистам.

  • Появляется возможность гибко управлять логированием, ожиданиями и ошибками без дублирования кода.

Пример на Java (упрощённая реализация Screenplay):

// Интерфейс команды
interface Action {
    void performAs(Actor actor);
}

// Класс актёра
class Actor {
    private final String name;

    public Actor(String name) {
        this.name = name;
    }

    public void attemptsTo(Action... actions) {
        for (Action action : actions) {
            action.performAs(this);
        }
    }

    public String getName() {
        return name;
    }
}

// Конкретные действия
class OpenPage implements Action {
    private final String url;

    public OpenPage(String url) {
        this.url = url;
    }

    @Override
    public void performAs(Actor actor) {
        System.out.println(actor.getName() + " открывает страницу: " + url);
    }
}

class EnterText implements Action {
    private final String field;
    private final String text;

    public EnterText(String field, String text) {
        this.field = field;
        this.text = text;
    }

    @Override
    public void performAs(Actor actor) {
        System.out.println(actor.getName() + " вводит '" + text + "' в поле " + field);
    }
}

class ClickButton implements Action {
    private final String button;

    public ClickButton(String button) {
        this.button = button;
    }

    @Override
    public void performAs(Actor actor) {
        System.out.println(actor.getName() + " нажимает кнопку " + button);
    }
}

// Пример сценария
public class ScreenplayExample {
    public static void main(String[] args) {
        Actor andrey = new Actor("Андрей");

        andrey.attemptsTo(
            new OpenPage("https://app.test"),
            new EnterText("логин", "test_user"),
            new EnterText("пароль", "123456"),
            new ClickButton("Войти")
        );
    }
}

Template Method

Описание:
Паттерн Template Method (Шаблонный метод) определяет скелет алгоритма в базовом классе и позволяет переопределять отдельные шаги в наследниках, не меняя структуру всего процесса.

Он используется, когда алгоритм всегда выполняется по одной и той же схеме, но детали шагов могут отличаться.

Пример из жизни: рецепт кофе — «вскипятить воду → добавить ингредиенты → налить в чашку».
Можно варьировать ингредиенты (капучино, латте, американо), но структура остаётся одинаковой.

Применение в автотестах:
В автоматизации этот паттерн часто используется для:

  • описания типовых тестовых сценариев (логин → действие → проверка результата);

  • настройки тест-хуков и шаблонов запуска (например, before/after шагов);

  • построения наследуемых тестов с разным поведением для разных ролей, устройств или окружений.

Пример на Java:

// Абстрактный класс с шаблонным методом
abstract class BaseTestTemplate {

    // Шаблонный метод
    public final void runTest() {
        setup();
        login();
        performAction();
        verifyResult();
        teardown();
    }

    protected void setup() {
        System.out.println("Инициализация окружения...");
    }

    protected abstract void login();
    protected abstract void performAction();
    protected abstract void verifyResult();

    protected void teardown() {
        System.out.println("Очистка данных и завершение теста...");
    }
}

// Конкретная реализация для роли "Администратор"
class AdminTest extends BaseTestTemplate {
    protected void login() {
        System.out.println("Авторизация под администратором");
    }

    protected void performAction() {
        System.out.println("Добавление нового пользователя");
    }

    protected void verifyResult() {
        System.out.println("Проверка, что пользователь успешно добавлен");
    }
}

// Конкретная реализация для роли "Пользователь"
class UserTest extends BaseTestTemplate {
    protected void login() {
        System.out.println("Авторизация под обычным пользователем");
    }

    protected void performAction() {
        System.out.println("Просмотр списка заказов");
    }

    protected void verifyResult() {
        System.out.println("Проверка, что список заказов отображается корректно");
    }
}

// Пример использования
public class TemplateMethodExample {
    public static void main(String[] args) {
        BaseTestTemplate adminTest = new AdminTest();
        BaseTestTemplate userTest = new UserTest();

        System.out.println("=== Тест для администратора ===");
        adminTest.runTest();

        System.out.println("\n=== Тест для пользователя ===");
        userTest.runTest();
    }
}

State

State
State

Описание:
Паттерн State (Состояние) позволяет объекту менять своё поведение в зависимости от внутреннего состояния, при этом не используя условные конструкции (if/else, switch) везде по коду.
Каждое состояние оформляется как отдельный класс, реализующий общий интерфейс поведения.

Пример из жизни: банкомат ведёт себя по-разному в зависимости от состояния — «вставлена карта», «ввод PIN-кода», «недостаточно средств».
Сам банкомат остаётся тем же объектом, но его реакции меняются.

Применение в автотестах:
В тестовой архитектуре этот паттерн особенно полезен, когда:

  • поведение приложения зависит от статуса — пользователя, заказа, платежа и т.д.;

  • нужно имитировать переходы между состояниями (например, draft → submitted → approved);

  • вы строите тестовый DSL, где объект «ведёт себя» по-разному на разных этапах;

  • хотите избежать множества if-ов в тестах и шагах.

Пример на Java:

// Общий интерфейс состояния
interface OrderState {
    void next(OrderContext context);
    void printStatus();
}

// Конкретные состояния
class CreatedState implements OrderState {
    public void next(OrderContext context) {
        context.setState(new PaidState());
    }
    public void printStatus() {
        System.out.println("Заказ создан, ожидает оплаты.");
    }
}

class PaidState implements OrderState {
    public void next(OrderContext context) {
        context.setState(new ShippedState());
    }
    public void printStatus() {
        System.out.println("Заказ оплачен, готов к отправке.");
    }
}

class ShippedState implements OrderState {
    public void next(OrderContext context) {
        System.out.println("Заказ уже отправлен. Переход невозможен.");
    }
    public void printStatus() {
        System.out.println("Заказ отправлен клиенту.");
    }
}

// Контекст, хранящий текущее состояние
class OrderContext {
    private OrderState state;

    public OrderContext() {
        this.state = new CreatedState();
    }

    public void setState(OrderState state) {
        this.state = state;
    }

    public void nextState() {
        state.next(this);
    }

    public void printStatus() {
        state.printStatus();
    }
}

// Пример использования
public class StateExample {
    public static void main(String[] args) {
        OrderContext order = new OrderContext();

        order.printStatus(); // "Заказ создан..."
        order.nextState();

        order.printStatus(); // "Заказ оплачен..."
        order.nextState();

        order.printStatus(); // "Заказ отправлен..."
        order.nextState();   // "Переход невозможен"
    }
}

Chain of Responsibility

Описание:
Паттерн Chain of Responsibility (Цепочка обязанностей) позволяет передавать запрос по цепочке обработчиков, где каждый обработчик решает — обрабатывать запрос или передать дальше.
Это избавляет от громоздких if-else конструкций и делает систему гибкой и расширяемой.

Пример из жизни: служба поддержки. Клиент пишет в чат → сначала отвечает бот, потом оператор, потом супервайзер. Каждый участник цепочки решает, может ли он обработать запрос, или передаёт его выше.

Применение в автотестах:
В тестовой архитектуре этот паттерн особенно полезен, когда нужно:

  • выполнять последовательные проверки (валидация данных, API-ответов и т.п.);

  • выстраивать цепочки тестовых шагов с возможностью прерывания при ошибке;

  • гибко добавлять или убирать обработчики, не изменяя общую структуру;

  • реализовать условную обработку событий — например, разные реакции на статусы ответа.

Пример на Java:

// Абстрактный обработчик
abstract class Handler {
    private Handler next;

    public Handler setNext(Handler next) {
        this.next = next;
        return next;
    }

    public void handle(Request request) {
        if (!process(request) && next != null) {
            next.handle(request);
        }
    }

    protected abstract boolean process(Request request);
}

// Объект запроса
class Request {
    private final String type;
    public Request(String type) { this.type = type; }
    public String getType() { return type; }
}

// Конкретные обработчики
class AuthHandler extends Handler {
    protected boolean process(Request request) {
        if ("auth".equals(request.getType())) {
            System.out.println("Авторизация обработана");
            return true;
        }
        return false;
    }
}

class ValidationHandler extends Handler {
    protected boolean process(Request request) {
        if ("validate".equals(request.getType())) {
            System.out.println("Проверка данных выполнена");
            return true;
        }
        return false;
    }
}

class DefaultHandler extends Handler {
    protected boolean process(Request request) {
        System.out.println("Неизвестный тип запроса");
        return true;
    }
}

// Пример использования
public class ChainExample {
    public static void main(String[] args) {
        Handler chain = new AuthHandler();
        chain.setNext(new ValidationHandler())
             .setNext(new DefaultHandler());

        chain.handle(new Request("auth"));
        chain.handle(new Request("validate"));
        chain.handle(new Request("unknown"));
    }
}

Memento

Описание:
Паттерн Memento (Снимок) позволяет сохранять и восстанавливать внутреннее состояние объекта без нарушения инкапсуляции. Идея — выделить отдельный «снимок» состояния (memento), который хранит необходимые данные, и предоставить внешнему коду возможность откатиться к этому снимку, не заглядывая внутрь объекта.

Ключевая идея:
Разделить обязанности: объект (Originator) создаёт снимок своего состояния; Caretaker хранит снимки и решает, когда их восстанавливать; внешние объекты не знают деталей состояния.

Применение в автотестах:

  • Сохранение состояния приложения перед опасной операцией (например, изменение данных в продакшн-подобном окружении) и откат при неудаче.

  • В тестах UI — возврат формы к предыдущему состоянию при проверке сложных сценариев.

  • В интеграционных тестах — сохранение конфигураций окружения и восстановление после теста.

  • Реализация «undo/redo» в тестируемом приложении и проверка корректности восстановления состояний.

Пример на Java (сохранение/восстановление состояния формы):

// Originator — объект, состояние которого нужно сохранять
public class Form {
    private String name;
    private String email;
    private boolean subscribed;

    public void setName(String name) { this.name = name; }
    public void setEmail(String email) { this.email = email; }
    public void setSubscribed(boolean subscribed) { this.subscribed = subscribed; }

    public void printState() {
        System.out.println("Form{name=" + name + ", email=" + email + ", subscribed=" + subscribed + "}");
    }

    // Создаёт снимок текущего состояния
    public FormMemento save() {
        return new FormMemento(name, email, subscribed);
    }

    // Восстанавливает состояние из снимка
    public void restore(FormMemento memento) {
        this.name = memento.getName();
        this.email = memento.getEmail();
        this.subscribed = memento.isSubscribed();
    }

    // Вложенный неизменяемый класс Memento
    public static final class FormMemento {
        private final String name;
        private final String email;
        private final boolean subscribed;

        private FormMemento(String name, String email, boolean subscribed) {
            this.name = name;
            this.email = email;
            this.subscribed = subscribed;
        }

        private String getName() { return name; }
        private String getEmail() { return email; }
        private boolean isSubscribed() { return subscribed; }
    }
}

// Caretaker — хранит снимки (может быть стеком для undo)
import java.util.Deque;
import java.util.ArrayDeque;

public class FormHistory {
    private final Deque<Form.FormMemento> history = new ArrayDeque<>();

    public void push(Form.FormMemento memento) {
        history.push(memento);
    }

    public Form.FormMemento pop() {
        return history.isEmpty() ? null : history.pop();
    }
}

// Пример использования в тесте
public class MementoExample {
    public static void main(String[] args) {
        Form form = new Form();
        FormHistory history = new FormHistory();

        form.setName("Initial");
        form.setEmail("init@example.com");
        form.setSubscribed(false);

        form.printState();

        // Сохраняем состояние перед серией изменений
        history.push(form.save());

        // Вносим изменения
        form.setName("User A");
        form.setEmail("a@example.com");
        form.setSubscribed(true);
        form.printState();

        // Решили откатиться
        Form.FormMemento snapshot = history.pop();
        if (snapshot != null) {
            form.restore(snapshot);
        }

        form.printState(); // состояние вернулось к Initial
    }
}

Антипаттерны в автотестах

Распространенные антипаттерны в автотестах
Распространенные антипаттерны в автотестах

1. God Test Class — монструозный тестовый класс

Что это: Один тестовый класс, который пытается проверить всё и сразу. Представьте файл на 1000+ строк, где вперемешку лежат тесты на логин, регистрацию, покупки, настройки профиля и т.д.

Пример проблемы:

@Test
void testEverything() {
    // тест логина
    loginPage.login("user", "pass");
    // тест поиска товара
    searchPage.search("laptop");
    // тест корзины
    cartPage.addItem();
    // тест оплаты
    paymentPage.pay();
    // и еще 20 несвязанных проверок...
}

Чем плох:

  • Нарушает принцип единственной ответственности

  • При падении одного теста падает весь "блок"

  • Невозможно понять, что именно тестируется

  • Сложно поддерживать и рефакторить

Как исправить:

@Test void userCanLogin() { /* только логин */ }
@Test void userCanSearchProducts() { /* только поиск */ }
@Test void userCanPurchaseItem() { /* только покупка */ }

2. Copy-Paste Locators — эпидемия дублирования

Что это: Один и тот же локатор, размноженный по десяткам тестовых методов.

Пример проблемы:

// Page Object или отдельный класс с локаторами
public class LoginLocators {
    public static final By 
        USERNAME = By.id("username"),
        PASSWORD = By.id("password"), 
        LOGIN_BTN = By.id("login");
}

@Test 
void testLogin() {
    WebDriver driver = new ChromeDriver();
    driver.findElement(LoginLocators.USERNAME).sendKeys("user");
    driver.findElement(LoginLocators.PASSWORD).sendKeys("pass");
    driver.findElement(LoginLocators.LOGIN_BTN).click();
    driver.quit();
}

Чем плох:

  • При изменении селектора нужно править 20+ мест

  • Легко пропустить одно из мест при рефакторинге

  • Код становится хрупким и трудно поддерживаемым

Как исправить:

// Page Object или отдельный класс с локаторами
public class LoginLocators {
    public static final SelenideElement 
        USERNAME = $("#username"),
        PASSWORD = $("#password"), 
        LOGIN_BTN = $("#login");
}

3. Hardcoded Waits — слепое ожидание

Что это: Использование фиксированных пауз вместо "умных" ожиданий.

Пример проблемы:

@Test
void testDynamicContent() {
    WebDriver driver = new ChromeDriver();
    WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));

    WebElement button = wait.until(ExpectedConditions.elementToBeClickable(By.id("button")));
    button.click();

    WebElement successMessage = wait.until(
        ExpectedConditions.visibilityOfElementLocated(By.id("result"))
    );
    assertEquals("Success", successMessage.getText());

    driver.quit();
}

Чем плох:

  • Тесты работают медленнее необходимого

  • На быстрых env тесты "спят" без дела

  • На медленных env тесты могут не дождаться

  • Ненадежно и непредсказуемо

Как исправить:

@Test void testDynamicContent() {
    // Хорошо - умные ожидания
    button.shouldBe(visible).click();
    successMessage.should(appear); // Selenide сам ждет
    
    // Или явные ожидания с условиями
    $("#result").shouldHave(text("Success"), Duration.ofSeconds(10));
}

Последствия антипаттернов:

  • Время рефакторинга увеличивается в 3-5 раз

  • Стабильность тестов падает на 40-60%

  • Скорость разработки замедляется экспоненциально

  • Выгорание команды из-за постоянной борьбы с хрупкими тестами

Заключение

Эволюция подходов к автоматизации тестирования
Эволюция подходов к автоматизации тестирования

Паттерны — это не академическая теория, а реальный инструмент, который помогает:

  • уменьшить дублирование,

  • ускорить рефакторинг,

  • облегчить вход новичкам в проект.

Комментарии (2)


  1. dopusteam
    07.10.2025 06:01

    User admin = UserFactory.createUser("admin");
    User guest = UserFactory.createUser("guest");

    Проще и безопаснее

    User admin = UserFactory.createAdmin();
    User guest = UserFactory.createGuest();


  1. AleksSharkov
    07.10.2025 06:01

    одно и тоже