Оглавление
Введение
Привет! В данной статье мы рассмотрим какие есть паттерны проектирования и как их можно использовать в написании автотестах. Данная статья будет полезна тем кто только начинает свой путь в автоматизации или повторяет материал перед собеседованием.
Когда мы пишем автотесты, кажется, что главное — это покрыть функционал. Но спустя несколько месяцев код обрастает костылями, тесты начинают падать «ни с того ни с сего», а новые люди в команде ломают голову, как вообще устроен проект.
Здесь на помощь приходят паттерны проектирования. Это проверенные временем решения типовых проблем, которые делают тестовый код поддерживаемым, гибким и понятным.
Классификация паттернов
Прежде чем делить паттерны на группы, важно понять, что это вообще такое.

Паттерн проектирования — это повторяемое архитектурное решение типовой задачи в программировании. Он не даёт готовый код, а описывает идею, которую можно адаптировать под конкретный проект.
Пример из жизни: если вы строите дом, то «однокомнатная квартира» или «таунхаус» — это паттерны планировки. Каждый архитектор может реализовать их по-своему, но принцип остаётся.
Основные группы паттернов
Порождающие
Эти паттерны отвечают за создание объектов. Они помогают избавиться от «new везде» и делают процесс гибче.В автотестах порождающие паттерны спасают от копипаста при создании тестовых пользователей, заказов, токенов и т.п.
Структурные
Эти паттерны помогают организовать связи между объектами и классами, чтобы код был чище и легче поддерживался.В автотестах структурные паттерны позволяют сделать код «как Lego»: из маленьких частей строится целая архитектура.
Поведенческие
Эти паттерны описывают алгоритмы взаимодействия объектов. Они делают систему гибкой и расширяемой.В автотестах поведенческие паттерны помогают описывать сценарии ближе к бизнес-логике и избегать монолитных «тестов-монстров».
Применение паттернов в автотестах
Когда мы строим проект автотестов, мы фактически создаём программную систему, а не просто набор скриптов. У такой системы есть архитектура, зависимости, слои и логика — всё как в настоящем приложении.
И чем больше тестов, тем быстрее всё превращается в хаос, если не придерживаться архитектурных принципов.
Здесь и приходят на помощь паттерны проектирования.
Паттерны помогают:
Разделять ответственность между классами (Single Responsibility).
Строить гибкую архитектуру, где легко добавлять новые тесты и сценарии.
Избегать дублирования и упрощать рефакторинг.
Делать тесты понятными для других членов команды.
Воспринимать тестовую систему как живой проект, а не как набор скриптов.
В мире тестирования паттерны применяются на всех уровнях:
Уровень | Примеры паттернов | Описание |
|---|---|---|
UI-тесты | Page Object, Screenplay, Facade | Упрощают работу со страницами и действиями пользователя |
API-тесты | Builder, Factory, Strategy | Помогают гибко формировать запросы и данные |
Infrastructure | Singleton, Proxy, Adapter | Управляют ресурсами и внешними зависимостями |
Тестовая логика | Template Method, State, Chain of Responsibility | Описывают последовательность шагов и поведения |
Порождающие паттерны
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 — это порождающий паттерн, который позволяет поэтапно создавать сложные объекты, отделяя процесс конструирования от конечного представления.
Идея в том, что один и тот же процесс построения можно использовать для создания разных вариаций объекта.
Паттерн особенно полезен, когда:
Объект имеет много параметров, часть из которых опциональна.
Хотим избегать длинных конструкторов с множеством аргументов.
Необходимо читаемое и безопасное создание объектов в тестах.
Применение в автотестах:
Генерация тестовых 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 (Абстрактная фабрика) предоставляет интерфейс для создания семейств связанных объектов без указания их конкретных классов.
Он помогает изолировать код от деталей реализации и делает возможным лёгкий выбор нужного набора объектов в зависимости от контекста.
Ключевая идея:
Создать «фабрику фабрик» — класс, который знает, какие конкретные фабрики нужно использовать для создания нужных объектов.
Применение в автотестах:
Когда тесты должны работать с разными платформами — например, 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 (Фасад) предоставляет упрощённый интерфейс к сложной системе классов или модулей.
Он скрывает внутреннюю реализацию и объединяет часто используемые операции в единый, понятный 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 (Стратегия) определяет семейство алгоритмов, инкапсулирует каждый из них и делает их взаимозаменяемыми.
Это позволяет изменять поведение программы во время выполнения, не изменяя клиентский код.
Ключевая идея:
Выделить алгоритмы в отдельные классы, которые реализуют общий интерфейс, и передавать нужную стратегию при запуске.
Применение в автотестах:
Используется для выбора способа авторизации (по паролю, токену, через 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 (Состояние) позволяет объекту менять своё поведение в зависимости от внутреннего состояния, при этом не используя условные конструкции (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%
Скорость разработки замедляется экспоненциально
Выгорание команды из-за постоянной борьбы с хрупкими тестами
Заключение

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