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

Паттерн проектирования — это повторяемое архитектурное решение типовой задачи в программировании. Он не даёт готовый код, а описывает идею, которую можно адаптировать под конкретный проект.
Пример из жизни: если вы строите дом, то «однокомнатная квартира» или «таунхаус» — это паттерны планировки. Каждый архитектор может реализовать их по-своему, но принцип остаётся.
Основные группы паттернов
-
Порождающие
Эти паттерны отвечают за создание объектов. Они помогают избавиться от «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%
Скорость разработки замедляется экспоненциально
Выгорание команды из-за постоянной борьбы с хрупкими тестами
Заключение

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