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

Паттерны — это та вещь, которая моментально выдает уровень культуры кода и понимание инженерных практик. Неудивительно, что на собеседованиях на позицию Automation QA любят покопаться в этой теме.

В этой статье я решил закрыть этот пробел. Вы найдете не только классификацию основных паттернов автоматизации, но и самый подробный, на мой взгляд, разбор каждого из них с примерами. А в конце поговорим про антипаттерны — чтобы вы знали, как делать не надо.

Добро пожаловать в обсуждение! Буду рад конструктивной критике и дополнениям.

Что такое паттерны в тестировании?

Паттерн — это многократно применимое решение часто встречающейся проблемы в конкретном контексте. В автоматизации тестирования паттерны помогают:

  • Структурировать код тестов так, чтобы его было легко читать и изменять.

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

  • Делать тесты стабильными — меньше ложных падений из-за таймингов, состояний окружения или изменений в продукте.

  • Ускорять разработку новых тестов за счёт готовых «кирпичиков».

  • Облегчать отладку и анализ результатов.

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

Чем паттерны отличаются от антипаттернов?

Если паттерны — это «хорошие практики», то антипаттерны — это распространённые ошибки и ловушки, в которые попадают разработчики тестов. Антипаттерны приводят к хрупкости, нечитаемости и низкой надёжности. Например:

  • Жёстко зашитые данные (hardcoded test data) — изменение одной цифры заставляет править десятки тестов.

  • Ожидания через sleep — тесты становятся медленными и нестабильными.

  • Отсутствие изоляции — тесты влияют друг на друга, и порядок запуска имеет значение.

Понимание антипаттернов не менее важно, чем знание паттернов: оно позволяет вовремя распознать проблему и применить правильное решение. В отдельной части статьи я подробно разберу самые вредные антипаттерны и способы борьбы с ними.

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

  1. Паттерны организации кода — отвечают за структуру тестового фреймворка, инкапсуляцию и переиспользование (Page Object, API Client Wrapper, Component Object, Fluent Interface, Builder, Factory, Dependency Injection).

  2. Паттерны управления данными — помогают создавать, подготавливать и изолировать тестовые данные (Test Data Factory, Data-Driven Testing, Transactional Tests, Snapshot Testing, Property-Based Testing).

  3. Паттерны обеспечения стабильности — борются с flaky-тестами, асинхронностью и сбоями (Waiter/Polling, Retry, Circuit Breaker).

  4. Паттерны работы с внешними зависимостями — изолируют тесты от сторонних сервисов и проверяют контракты (API Mocking, Mock Service, Schema Validation, Contract Testing).

  5. Паттерны оптимизации и параллельного выполнения — ускоряют прогон тестов (Parallel Execution, Lazy Setup, Proxy для логирования).

  6. Паттерны расширения и отчётности — добавляют дополнительные возможности без изменения логики тестов (Observer, Template Method, Decorator).

  7. Специфические паттерны для UI — визуальное тестирование, кросс-браузерность, явные ожидания.

  8. Дополнительные общие паттерны — Singleton, Command, Strategy, Null Object и другие, которые могут пригодиться в иных ситуациях.

В статье последовательно пройдём по всем группам и рассмотрим более 30 паттернов с примерами кода.

Почему важно знать паттерны как для UI, так и для API?

Автоматизация UI и API часто идёт рука об руку, и многие паттерны применимы к обоим уровням:

  • Builder удобен для формирования сложных JSON-запросов к API, но точно так же он помогает создавать объекты пользователей для UI-тестов.

  • Waiter/Polling критичен для проверки асинхронных обновлений в интерфейсе, но без него не обойтись и при тестировании Kafka или БД.

  • Factory централизует создание тестовых данных, независимо от того, используются они в UI или API.

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

На каких языках и инструментах будут примеры?

Для каждого паттерна я приведу фрагменты кода на трёх популярных языках программирования:

  • Java — классика корпоративной разработки, богатая экосистема (Selenium, RestAssured, JUnit, TestNG, Spring, WireMock, Pact).

  • JavaScript/TypeScript — лидер в веб-технологиях и современных фреймворках (Playwright, Cypress, Jest, Axios).

  • Python — любимый язык для быстрого прототипирования и мощных фреймворков (pytest, Requests, FastAPI, Pydantic).

Такой мультиязычный подход поможет читателям, работающим в разных стеках, найти знакомые примеры и понять, как перенести паттерн в свой проект. Кроме того, я покажу вариативность реализации: например, паттерн Retry может быть настроен в конфигурационном файле (как в Playwright или pytest-rerunfailures) или реализован вручную с помощью декоратора. Ожидания (Waiter) могут быть встроенными в инструмент (автоожидание Playwright) или требовать самостоятельной реализации (например, для проверки записей в БД).

Сводная таблица паттернов

Название

Категория

Важность

Применение

Суть

1

Page Object Model (POM)

Организация кода

★★★★★

UI

Инкапсуляция страниц и их элементов

2

Component Object

Организация кода

★★★★☆

UI

Вынесение повторяющихся блоков в компоненты

3

API Client Wrapper

Организация кода

★★★★★

API

Класс-клиент для API, инкапсулирующий эндпоинты

4

Fluent Interface

Организация кода

★★★☆☆

Оба

Цепочки методов для читаемого кода

5

Builder

Данные

★★★★☆

Оба

Пошаговое создание сложных объектов

6

Factory / Object Mother

Данные

★★★★☆

Оба

Централизованное создание тестовых объектов

7

Dependency Injection

Организация кода

★★★★★

Оба

Внедрение зависимостей через фикстуры/контейнеры

8

Custom Fixtures / Hooks

Организация кода

★★★★★

Оба

Переиспользуемая логика подготовки/очистки

9

Faker (генерация данных)

Данные

★★★★☆

Оба

Реалистичные случайные данные

10

Property-Based Testing

Данные

★★★☆☆

API (реже UI)

Автоматическая генерация множества вариантов

11

Data-Driven Testing

Данные

★★★��★

Оба

Параметризация тестов разными данными

12

Transactional Tests / Clean State

Изоляция

★★★★★

API (БД)

Откат транзакций или очистка данных между тестами

13

Snapshot Testing

Проверки

★★★★☆

Оба

Сравнение с эталоном (JSON, скриншоты)

14

Waiter / Polling

Стабильность

★★★★★

Оба

Активное ожидание условия с retry

15

Retry

Стабильность

★★★★☆

Оба

Автоматический перезапуск упавших тестов

16

Circuit Breaker

Стабильность

★★★☆☆

Оба

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

17

API Mocking

Внешние зависимости

★★★★★

UI / API

Перехват и подмена HTTP-запросов

18

Mock Service (самописный)

Внешние зависимости

★★★★☆

API

Отдельный сервис-заглушка с админским API

19

Schema Validation

Проверки

★★★★☆

API

Валидация ответа по JSON Schema

20

Contract Testing (Pact)

Внешние зависимости

★★★★★

API

Consumer-driven contract testing

21

Parallel Execution

Оптимизация

★★★★☆

Оба

Запуск тестов в несколько потоков/воркеров

22

Lazy Setup / Caching

Оптимизация

★★★★☆

Оба

Однократное создание тяжёлых ресурсов

23

Proxy (логирование, метрики)

Расширение

★★★☆☆

Оба

Обёртка для добавления сквозной функциональности

24

Observer (слушатели событий)

Расширение

★★★★☆

Оба

Реакция на события жизненного цикла тестов

25

Template Method

Организация кода

★★★☆☆

Оба

Базовый класс, задающий скелет теста

26

Visual Testing

Проверки

★★★★☆

UI

Сравнение скриншотов с эталоном

27

Cross-Browser & Mobile

Окружение

★★★★☆

UI

Запуск тестов в разных браузерах/устройствах

28

Singleton

Организация кода

★★★☆☆

Оба

Единственный экземпляр класса (конфиг, менеджер)

29

Command

Расширение

★★★☆☆

Оба

Инкапсуляция действия как объекта (с отменой)

30

Strategy

Данные / Поведение

★★★☆☆

Оба

Взаимозаменяемые алгоритмы (генерация, логин)

Расшифровка категорий:

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

  • Данные – управление тестовыми данными (генерация, подготовка, параметризация).

  • Стабильность – борьба с flaky-тестами, асинхронностью, сбоями.

  • Проверки – способы валидации результатов (схемы, снэпшоты, визуал).

  • Внешние зависимости – изоляция от внешних сервисов (моки, контракты).

  • Оптимизация – ускорение тестов и эффективное использование ресурсов.

  • Расширение – дополнительные возможности (логирование, метрики, события).

  • Окружение – работа с разными браузерами/устройствами.

Часть 1. Паттерны организации кода и архитектуры тестов

(паттерны, которые делают код поддерживаемым и переиспользуемым)

1. Page Object Model (POM)

Название и синонимы

Page ObjectPage Object ModelPOM. Иногда встречается термин Page Object Pattern.

Проблема, которую решает

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

Суть паттерна

Page Object Model предлагает для каждой страницы или значимой части интерфейса создать отдельный класс, который:

  • Инкапсулирует селекторы элементов страницы (поля, кнопки, ссылки).

  • Предоставляет методы, соответствующие действиям пользователя на этой странице (например, login()addToCart()checkout()).

  • Скрывает детали реализации (типы локаторов, способы взаимодействия) от тестов.

Тесты взаимодействуют с этими методами, что делает их высокоуровневыми, читаемыми и устойчивыми к изменениям. При изменении интерфейса достаточно поправить только соответствующий Page Object, а не все тесты.

Реализация в UI

Рассмотрим классическую реализацию Page Object для страницы логина на трёх языках с использованием популярных инструментов.

Java (Selenium WebDriver)

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;

public class LoginPage {
    private WebDriver driver;
    private By usernameInput = By.id("user-name");
    private By passwordInput = By.id("password");
    private By loginButton = By.id("login-button");

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

    public void open() {
        driver.get("https://www.saucedemo.com");
    }

    public void enterUsername(String username) {
        driver.findElement(usernameInput).sendKeys(username);
    }

    public void enterPassword(String password) {
        driver.findElement(passwordInput).sendKeys(password);
    }

    public void clickLogin() {
        driver.findElement(loginButton).click();
    }

    public void login(String username, String password) {
        enterUsername(username);
        enterPassword(password);
        clickLogin();
    }
}

TypeScript (Playwright)

import { Page, Locator } from '@playwright/test';

export class LoginPage {
    readonly page: Page;
    readonly usernameInput: Locator;
    readonly passwordInput: Locator;
    readonly loginButton: Locator;

    constructor(page: Page) {
        this.page = page;
        this.usernameInput = page.locator('#user-name');
        this.passwordInput = page.locator('#password');
        this.loginButton = page.locator('#login-button');
    }

    async open() {
        await this.page.goto('https://www.saucedemo.com');
    }

    async login(username: string, password: string) {
        await this.usernameInput.fill(username);
        await this.passwordInput.fill(password);
        await this.loginButton.click();
    }
}

Python (Playwright)

from playwright.sync_api import Page

class LoginPage:
    def __init__(self, page: Page):
        self.page = page
        self.username_input = page.locator("#user-name")
        self.password_input = page.locator("#password")
        self.login_button = page.locator("#login-button")

    def open(self):
        self.page.goto("https://www.saucedemo.com")

    def login(self, username: str, password: str):
        self.username_input.fill(username)
        self.password_input.fill(password)
        self.login_button.click()

Вариативность реализации

  1. Классический POM (Java + Selenium) – каждый метод явно ищет элементы через driver.findElement. Часто используется с паттерном PageFactory (@FindBy) для автоматической инициализации элементов.

  2. Ленивая инициализация (Playwright, Cypress) – локаторы создаются один раз при создании объекта, но поиск в DOM происходит только при взаимодействии. Это повышает производительность и удобство.

  3. Базовый класс (BasePage) – создаётся абстрактный класс BasePage, содержащий общие методы (ожидание загрузки, скриншоты, доступ к хедеру/футеру), от которого наследуются все страницы.

  4. Интеграция с DI / фикстурами – в современных фреймворках (Playwright, pytest) объекты страниц создаются через фикстуры и передаются в тесты, что упрощает управление зависимостями и очистку состояния.

  5. Компонентный подход – внутри Page Object могут использоваться отдельные классы-компоненты для повторяющихся блоков (например, HeaderProductTile). Это развитие POM, называемое Component Object.

Когда применять, а когда нет

  • Применять всегда, когда вы пишете UI-автотесты для приложения с несколькими страницами или сложным интерфейсом. Это база, без которой поддерживаемый фреймворк невозможен.

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

Связь с другими паттернами

  • Component Object – логическое расширение POM для вынесения повторяющихся частей.

  • Fluent Interface – методы Page Object м��гут возвращать this, чтобы строить цепочки вызовов (например, loginPage.enterUsername("user").enterPassword("pass").clickLogin()).

  • Factory – может использоваться для создания страниц с заданным состоянием (например, страница товара с уже добавленным товаром).

  • Dependency Injection – страницы часто получают через DI (фикстуры), что упрощает тесты и управление жизненным циклом.

  • Singleton – редко для страниц, но иногда применяется для менеджера страниц, чтобы не создавать одни и те же объекты многократно.

Пример кода с использованием базового класса (Java)

public abstract class BasePage {
    protected WebDriver driver;

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

    public abstract void waitForPageLoaded();
}

public class LoginPage extends BasePage {
    // ... конструктор, селекторы
    @Override
    public void waitForPageLoaded() {
        wait.until(ExpectedConditions.visibilityOfElementLocated(usernameInput));
    }
}

Резюме

Page Object Model – фундаментальный паттерн UI-автоматизации, который обеспечивает инкапсуляцию деталей страницы и повышает читаемость тестов. Он является «золотым стандартом» и обязателен к применению в любом серьёзном проекте.

2. Component Object (Page Components)

Название и синонимы

Component ObjectPage ComponentUI ComponentFragments. Иногда называют Widget Object.

Проблема, которую решает

Даже при использовании Page Object Model страницы могут стать слишком большими и сложными, если они содержат множество повторяющихся блоков. Например, хедер, футер, карточка товара, меню навигации — эти элементы присутствуют на нескольких страницах, и их логика дублируется в разных Page Object'ах. Если изменится структура карточки товара, придётся править все страницы, где она используется. Кроме того, внутри одной страницы может быть несколько экземпляров одного и того же компонента (например, список карточек), и обращаться к каждому по отдельности неудобно.

Суть паттерна

Component Object предлагает выносить повторяющиеся блоки интерфейса в отдельные классы-компоненты. Каждый такой класс:

  • Инкапсулирует селекторы и действия, специфичные для данного компонента (например, карточка товара: название, цена, кнопка «Добавить в корзину»).

  • Может принимать корневой локатор, чтобы идентифицировать конкретный экземпляр компонента на странице (особенно если их несколько).

  • Предоставляет методы для взаимодействия с компонентом (addToCart()getPrice()getName()).

Page Object'ы используют эти компоненты как строительные блоки, делегируя им работу с соответствующими элементами. Это устраняет дублирование и делает код более модульным.

Реализация в UI

Java (Selenium)

Рассмотрим компонент ProductTile для карточки товара.

import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;

public class ProductTile {
    private WebElement root;

    public ProductTile(WebElement root) {
        this.root = root;
    }

    public String getName() {
        return root.findElement(By.className("inventory_item_name")).getText();
    }

    public String getPrice() {
        return root.findElement(By.className("inventory_item_price")).getText();
    }

    public void addToCart() {
        root.findElement(By.cssSelector("button.btn_inventory")).click();
    }
}

Использование в Page Object'е страницы товаров:

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import java.util.List;
import java.util.stream.Collectors;

public class InventoryPage {
    private WebDriver driver;
    private By productTiles = By.cssSelector(".inventory_item");

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

    public List<ProductTile> getProducts() {
        List<WebElement> elements = driver.findElements(productTiles);
        return elements.stream().map(ProductTile::new).collect(Collectors.toList());
    }

    public ProductTile getProductByName(String name) {
        return getProducts().stream()
                .filter(p -> p.getName().equals(name))
                .findFirst()
                .orElseThrow(() -> new RuntimeException("Product not found: " + name));
    }
}

TypeScript (Playwright)

Компонент ProductTile с использованием Playwright:

import { Locator, Page } from '@playwright/test';

export class ProductTile {
    constructor(private root: Locator) {}

    getName(): Locator {
        return this.root.locator('.inventory_item_name');
    }

    getPrice(): Locator {
        return this.root.locator('.inventory_item_price');
    }

    async addToCart(): Promise<void> {
        await this.root.locator('button.btn_inventory').click();
    }
}

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

import { Page, Locator } from '@playwright/test';
import { ProductTile } from './ProductTile';

export class InventoryPage {
    readonly page: Page;
    readonly productTiles: Locator;

    constructor(page: Page) {
        this.page = page;
        this.productTiles = page.locator('.inventory_item');
    }

    getProducts(): ProductTile[] {
        // Playwright не умеет напрямую создавать массив объектов из локаторов,
        // поэтому используем count() и цикл.
        const tiles: ProductTile[] = [];
        // Более элегантно через map, но нужно дождаться стабильности.
        return this.productTiles.all().then(elements => 
            elements.map(element => new ProductTile(element))
        );
    }

    async getProductByName(name: string): Promise<ProductTile> {
        const products = await this.getProducts();
        for (const product of products) {
            if (await product.getName().textContent() === name) {
                return product;
            }
        }
        throw new Error(`Product with name "${name}" not found`);
    }
}

Python (Playwright)

Аналогично:

from playwright.sync_api import Page, Locator

class ProductTile:
    def __init__(self, root: Locator):
        self.root = root

    def get_name(self) -> str:
        return self.root.locator(".inventory_item_name").text_content()

    def get_price(self) -> str:
        return self.root.locator(".inventory_item_price").text_content()

    def add_to_cart(self):
        self.root.locator("button.btn_inventory").click()


class InventoryPage:
    def __init__(self, page: Page):
        self.page = page
        self.product_tiles = page.locator(".inventory_item")

    def get_products(self) -> list[ProductTile]:
        # Locator.all() возвращает список Locator для каждого элемента
        elements = self.product_tiles.all()
        return [ProductTile(el) for el in elements]

    def get_product_by_name(self, name: str) -> ProductTile:
        for product in self.get_products():
            if product.get_name() == name:
                return product
        raise ValueError(f"Product with name {name} not found")

Вариативность реализации

  1. Компонент как обёртка над WebElement/Locator – самый распространённый подход, показанный выше.

  2. Компонент с собственными сложными селекторами – если структура компонента сложна, внутри компонента могут быть приватные локаторы, построенные относительно корневого.

  3. Динамический поиск – корневой локатор может быть передан в конструктор и использоваться для поиска дочерних элементов в момент вызова метода.

  4. Наследование компонентов – можно создать базовый класс BaseComponent с общими методами (например, isVisible()click()) для всех компонентов.

  5. Фабрика компонентов – если страница содержит список компонентов, можно создать метод getComponents() или getComponent(index), возвращающий экземпляр компонента для каждого элемента списка.

  6. Составные компоненты – компонент может содержать вложенные компоненты (например, Header содержит SearchBar и CartIcon).

Когда применять, а когда нет

  • Применять, когда в интерфейсе есть повторяющиеся блоки (карточки, строки таблицы, элементы меню) или блоки, присутствующие на нескольких страницах (хедер, футер, сайдбар).

  • Не применять, если блок используется только один раз и очень прост (например, одна кнопка). Однако даже в этом случае вынесение может быть оправдано, если логика блока сложна.

  • Компоненты особенно полезны в современных SPA, где один и тот же компонент может рендериться в разных местах приложения.

Связь с другими паттернами

  • Page Object Model – компоненты являются «строительными блоками» внутри Page Object'ов. Хороший POM всегда использует компоненты для крупных повторяющихся частей.

  • Fluent Interface – методы компонента могут возвращать this, чтобы строить цепочки (например, productTile.addToCart().verifyCartBadge()).

  • Factory – может использоваться для создания компонентов с заданным состоянием.

  • Builder – может применяться для создания сложных компонентов с множеством полей (редко, но возможно).

Пример кода с использованием базового класса (TypeScript)

export abstract class BaseComponent {
    constructor(protected root: Locator) {}

    async isVisible(): Promise<boolean> {
        return this.root.isVisible();
    }

    async click(): Promise<void> {
        await this.root.click();
    }
}

export class Header extends BaseComponent {
    readonly cartIcon: Locator;

    constructor(root: Locator) {
        super(root);
        this.cartIcon = root.locator('.shopping_cart_link');
    }

    async goToCart(): Promise<void> {
        await this.cartIcon.click();
    }
}

Резюме

Component Object – это естественное расширение Page Object Model, которое позволяет управлять сложностью интерфейса, выделяя повторяющиеся блоки в переиспользуемые классы. Паттерн повышает модульность, уменьшает дублирование и упрощает поддержку тестов при изменении UI. В сочетании с POM он является обязательным инструментом для масштабируемых фреймворков UI-автоматизации.

3. API Client Wrapper (API Object / Service Object)

Название и синонимы

API Client WrapperAPI ObjectService ObjectAPI Client LayerHTTP Client Wrapper.

Проблема, которую решает

В автоматизации API-тестов часто встречается код, в котором HTTP-запросы строятся прямо в тестах: URL, параметры, заголовки, тело запроса – всё это "зашито" в каждом тесте. Это приводит к множеству проблем:

  • Дублирование кода: одни и те же endpoint'ы и параметры повторяются в разных тестах.

  • Сложность поддержки: при изменении API (например, добавлении обязательного заголовка или изменении структуры URL) приходится править все тесты, использующие этот endpoint.

  • Низкая читаемость: тесты перегружены техническими деталями, вместо бизнес-действий видно httpClient.get("/api/v1/parts/search?brand=BMW&model=X5").

  • Отсутствие единого места для логирования, обработки ошибок, ретраев.

Суть паттерна

API Client Wrapper предлагает создать отдельный класс (или набор классов), который:

  • Инкапсулирует базовый URLзаголовкиаутентификацию и другие общие настройки HTTP-клиента.

  • Предоставляет методы, соответствующие каждому endpoint'у API, с понятными именами и параметрами, скрывая детали построения запроса (URL, параметры, сериализацию).

  • Может включать общую логику: логирование запросов/ответов, обработку ошибок, повторные попытки, валидацию ответов.

  • Возвращает сырой ответ (например, Response объект) или уже десериализованные данные (DTO), предоставляя тестам выбор уровня проверки.

Тесты взаимодействуют с методами клиента, что делает их высокоуровневыми и устойчивыми к изменениям API. При изменении endpoint'а правки вносятся только в соответствующий метод клиента.

Реализация в API

Рассмотрим реализацию для API поиска автозапчастей на трёх языках.

Java (RestAssured)

import io.restassured.RestAssured;
import io.restassured.response.Response;
import io.restassured.specification.RequestSpecification;
import java.util.Map;

public class PartsApiClient {
    private final String baseUrl;
    private final RequestSpecification spec;

    public PartsApiClient(String baseUrl) {
        this.baseUrl = baseUrl;
        this.spec = RestAssured.given()
                .baseUri(baseUrl)
                .header("Content-Type", "application/json")
                .log().ifValidationFails(); // логирование при падении
    }

    public Response searchParts(String brand, String model, String generation, String category) {
        return spec
                .queryParam("brand", brand)
                .queryParam("model", model)
                .queryParam("generation", generation)
                .queryParam("category", category)
                .get("/api/v1/parts/search");
    }

    // Можно добавить методы для других endpoint'ов
    public Response getPartById(String partId) {
        return spec.get("/api/v1/parts/" + partId);
    }
}

TypeScript (axios)

import axios, { AxiosInstance, AxiosResponse } from 'axios';

export class PartsApiClient {
    private client: AxiosInstance;

    constructor(baseURL: string) {
        this.client = axios.create({
            baseURL,
            headers: { 'Content-Type': 'application/json' },
        });

        // Добавляем интерсепторы для логирования
        this.client.interceptors.request.use(request => {
            console.log('Starting Request', request);
            return request;
        });
    }

    async searchParts(
        brand: string,
        model: string,
        generation?: string,
        category?: string
    ): Promise<AxiosResponse> {
        const params: any = { brand, model };
        if (generation) params.generation = generation;
        if (category) params.category = category;

        return this.client.get('/api/v1/parts/search', { params });
    }

    async getPartById(partId: string): Promise<AxiosResponse> {
        return this.client.get(`/api/v1/parts/${partId}`);
    }
}

Python (httpx с async)

import httpx
from typing import Optional, Dict

class PartsAPIClient:
    def __init__(self, client: httpx.AsyncClient):
        self._client = client

    async def search_parts(
        self,
        brand: str,
        model: str,
        generation: Optional[str] = None,
        category: Optional[str] = None
    ) -> httpx.Response:
        params = {"brand": brand, "model": model}
        if generation:
            params["generation"] = generation
        if category:
            params["category"] = category

        return await self._client.get("/api/v1/parts/search", params=params)

    async def get_part_by_id(self, part_id: str) -> httpx.Response:
        return await self._client.get(f"/api/v1/parts/{part_id}")

Вариативность реализации

  1. Сырой клиент vs десериализованные DTO – можно возвращать объект ответа (например, Response в RestAssured или httpx.Response), давая тестам полный доступ, или сразу десериализовать в POJO/DTO (например, PartSearchResponse) для удобства проверок. Часто выбирают гибрид: методы клиента возвращают ответ, но в тестах есть хелперы для валидации.

  2. Синхронный / асинхронный – в Python и JS популярны асинхронные клиенты (httpx, axios поддерживают оба стиля). В Java RestAssured синхронный, но можно использовать асинхронные варианты.

  3. Статический vs динамический – клиент может быть синглтоном или создаваться через DI. В современных фреймворках предпочитают DI (например, фикстуры в pytest).

  4. Генерация клиента из OpenAPI/Swagger – можно автоматически генерировать классы клиента по спецификации (OpenAPI Generator, Swagger Codegen). Это даёт всегда актуальный клиент, но требует доверия к генератору и иногда ручной доработки.

  5. Обёртка с ретраями и circuit breaker – клиент может включать логику повторных попыток при временных ошибках, используя паттерны Retry и Circuit Breaker.

  6. Поддержка разных форматов – JSON, XML, multipart. Клиент инкапсулирует сериализацию/десериализацию.

  7. Логирование и мониторинг – через интерсепторы (axios) или фильтры (RestAssured) можно логировать все запросы, что облегчает отладку.

Когда применять, а когда нет

  • Применять всегда при написании API-тестов для любого более-менее сложного API. Это база, без которой тесты быстро превращаются в "спагетти".

  • Не применять только в исключительных случаях, например, для одного-двух тривиальных запросов в рамках исследования API. Но даже тогда лучше создать простой клиент – это дисциплинирует.

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

Связь с другими паттернами

  • Builder Pattern – часто используется вместе с API Client Wrapper для построения сложных запросов (например, SearchPartsRequestBuilder). Клиент принимает готовый объект запроса.

  • Factory Pattern – может создавать предопределённые запросы (например, PartsRequestFactory.bmwX5Request()).

  • Dependency Injection – клиент внедряется в тесты через фикстуры (pytest) или конструкторы (Java), что упрощает замену на мок-клиент.

  • Schema Validation – ответы от клиента можно валидировать против JSON Schema.

  • Retry / Circuit Breaker – могут быть встроены в клиент для повышения надёжности.

  • Proxy Pattern – клиент может быть обёрнут для логирования или сбора метрик.

Пример кода с использованием DTO (Java)

// DTO
public class PartSearchResponse {
    private List<Part> parts;
    private int total;
    private String source;
    // геттеры/сеттеры
}

// Метод клиента, возвращающий DTO
public PartSearchResponse searchParts(String brand, String model) {
    return spec
        .queryParam("brand", brand)
        .queryParam("model", model)
        .get("/api/v1/parts/search")
        .as(PartSearchResponse.class);
}

Резюме

API Client Wrapper – фундаментальный паттерн для организации API-тестов. Он инкапсулирует детали взаимодействия с API, предоставляя чистый и понятный интерфейс для тестов. Паттерн повышает поддерживаемость, переиспользуемость и читаемость кода, а также открывает путь для применения других паттернов (Builder, Retry, Schema Validation). Это обязательный элемент любого серьёзного фреймворка для тестирования API, аналогичный Page Object в UI-автоматизации.

4. Fluent Interface (цепочки методов)

Название и синонимы

Fluent InterfaceMethod ChainingFluent API. Иногда называют Fluent Builder, когда применяется в сочетании с паттерном Builder.

Проблема, которую решает

При написании тестов и тестовых утилит часто приходится выполнять несколько последовательных действий над одним объектом или настраивать объект с большим количеством параметров. Код без Fluent Interface выглядит громоздко:

// Без fluent
UserBuilder builder = new UserBuilder();
builder.setUsername("john");
builder.setPassword("secret");
builder.setEmail("john@example.com");
User user = builder.build();

// С fluent
User user = new UserBuilder()
    .withUsername("john")
    .withPassword("secret")
    .withEmail("john@example.com")
    .build();

Второй вариант гораздо читаемее, компактнее и напоминает предметно-ориентированный язык (DSL). Fluent Interface решает проблему избыточного кода и низкой выразительности при настройке объектов и выполнении последовательных операций.

Суть паттерна

Fluent Interface – это стиль проектирования API, при котором методы объекта возвращают сам объект (this), что позволяет выстраивать цепочки вызовов. Каждый метод выполняет некоторое действие и возвращает тот же объект для следующего вызова.

В контексте автоматизации тестирования Fluent Interface применяется для:

  • Построения сложных запросов (API) с помощью билдеров.

  • Выполнения последовательных действий в UI-тестах (например, заполнение формы шаг за шагом).

  • Создания читаемых сценариев в тестах, где каждый шаг возвращает контекст для следующего шага.

Важно, что Fluent Interface не меняет логику работы, а лишь предоставляет удобный синтаксис.

Реализация

1. Fluent Builder для тестовых данных (API и UI)

Самый частый пример – построитель запросов или тестовых объектов.

Java (пример с UserBuilder)

public class UserBuilder {
    private String username;
    private String password;
    private String email;

    public UserBuilder withUsername(String username) {
        this.username = username;
        return this;
    }

    public UserBuilder withPassword(String password) {
        this.password = password;
        return this;
    }

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

    public User build() {
        return new User(username, password, email);
    }
}

TypeScript

class UserBuilder {
    private username: string = '';
    private password: string = '';
    private email: string = '';

    withUsername(username: string): this {
        this.username = username;
        return this;
    }

    withPassword(password: string): this {
        this.password = password;
        return this;
    }

    withEmail(email: string): this {
        this.email = email;
        return this;
    }

    build(): User {
        return new User(this.username, this.password, this.email);
    }
}

Python

class UserBuilder:
    def __init__(self):
        self._username = None
        self._password = None
        self._email = None

    def with_username(self, username: str) -> 'UserBuilder':
        self._username = username
        return self

    def with_password(self, password: str) -> 'UserBuilder':
        self._password = password
        return self

    def with_email(self, email: str) -> 'UserBuilder':
        self._email = email
        return self

    def build(self) -> 'User':
        return User(self._username, self._password, self._email)

2. Fluent Interface в Page Object (UI)

Методы Page Object могут возвращать this, чтобы позволить цепочки действий на одной странице.

Java (Selenium)

public class LoginPage {
    // ... элементы

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

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

    public InventoryPage clickLogin() {
        driver.findElement(loginButton).click();
        return new InventoryPage(driver); // переход на другую страницу
    }
}

// Использование:
new LoginPage(driver)
    .enterUsername("standard_user")
    .enterPassword("secret_sauce")
    .clickLogin();

В этом примере методы enterUsername и enterPassword возвращают this, позволяя строить цепочку, а clickLogin возвращает следующий Page Object.

TypeScript (Playwright)

class LoginPage {
    // ...

    async enterUsername(username: string): Promise<this> {
        await this.usernameInput.fill(username);
        return this;
    }

    async enterPassword(password: string): Promise<this> {
        await this.passwordInput.fill(password);
        return this;
    }

    async clickLogin(): Promise<InventoryPage> {
        await this.loginButton.click();
        return new InventoryPage(this.page);
    }
}

Python (Playwright)

class LoginPage:
    # ...

    def enter_username(self, username: str) -> 'LoginPage':
        self.username_input.fill(username)
        return self

    def enter_password(self, password: str) -> 'LoginPage':
        self.password_input.fill(password)
        return self

    def click_login(self) -> 'InventoryPage':
        self.login_button.click()
        return InventoryPage(self.page)

3. Fluent Interface для выполнения сценариев (например, проверки)

Можно создать обёртку, которая позволяет строить цепочки проверок.

Java

public class ResponseValidator {
    private final Response response;

    public ResponseValidator(Response response) {
        this.response = response;
    }

    public ResponseValidator statusCode(int expected) {
        assertThat(response.statusCode()).isEqualTo(expected);
        return this;
    }

    public ResponseValidator bodyContains(String key, Object value) {
        // проверка тела
        return this;
    }
}

// Использование:
new ResponseValidator(response)
    .statusCode(200)
    .bodyContains("brand", "BMW");

Вариативность реализации

  1. Простая цепочка с возвратом this – методы модифицируют состояние и возвращают тот же объект.

  2. Цепочка с переходом между объектами – как в примере с Page Object, где один метод возвращает другой объект (следующую страницу). Это тоже считается fluent, но уже не строгий возврат this.

  3. Fluent Builder с кастомными шагами – можно создавать билдеры, которые на разных этапах предлагают разные наборы методов (например, StepBuilder). Это реализуется через интерфейсы.

  4. Использование декораторов – в Python можно использовать декораторы для автоматического возврата self, но обычно проще написать явно.

  5. Fluent-интерфейсы в тестовых фреймворках – многие современные фреймворки предоставляют fluent API (например, Playwright для действий на странице, RestAssured для построения запросов).

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

Когда применять, а когда нет

  • Применять, когда есть объект с множеством настраиваемых параметров (билдеры) или последовательность действий над одним объектом. Это улучшает читаемость и уменьшает дублирование кода.

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

  • Не применять, если цепочка получается слишком длинной и сложной (более 5-7 вызовов) – это может ухудшить читаемость. В таких случаях лучше разбить на отдельные шаги.

  • Не применять, если методы имеют побочные эффекты, нарушающие принцип единственной ответственности, или если возврат this может привести к ошибкам при параллельном выполнении (хотя в тестах это редкость).

Связь с другими паттернами

  • Builder – наиболее тесная связь: Fluent Interface – это стандартный способ реализации Builder'а.

  • Page Object – может использовать Fluent Interface для методов, работающих на одной странице.

  • Command – цепочки команд можно строить с помощью fluent-интерфейса.

  • Factory – фабрики редко бывают fluent, но могут сочетаться с билдерами.

  • Proxy – прокси может перехватывать вызовы и возвращать this, но это уже экзотика.

Пример кода (расширенный)

Приведём пример сложного билдера для API-запроса из реального проекта (аналогично PartsAPIClient и Builder из API.pdf).

Python (асинхронный билдер)

class SearchPartsRequestBuilder:
    def __init__(self):
        self._params = {}

    def with_vehicle(self, brand: str, model: str, generation: str = None) -> 'SearchPartsRequestBuilder':
        self._params['brand'] = brand
        self._params['model'] = model
        if generation:
            self._params['generation'] = generation
        return self

    def with_category(self, category: str) -> 'SearchPartsRequestBuilder':
        self._params['category'] = category
        return self

    def with_page(self, page: int, size: int = 20) -> 'SearchPartsRequestBuilder':
        self._params['page'] = page
        self._params['size'] = size
        return self

    def build(self) -> dict:
        return {k: v for k, v in self._params.items() if v is not None}

# Использование в тесте:
request = (SearchPartsRequestBuilder()
           .with_vehicle("BMW", "X5", generation="E70")
           .with_category("brakes")
           .with_page(1)
           .build())

Резюме

Fluent Interface – это приём, делающий код тестов и вспомогательных классов выразительным и лаконичным. Он широко применяется в билдерах, Page Object'ах и API-клиентах. Правильное использование Fluent Interface приближает тесты к естественному языку, облегчая их понимание и поддержку. В сочетании с Builder и другими паттернами он становится незаменимым инструментом в арсенале автоматизатора.

5. Builder Pattern (Строитель)

Название и синонимы

BuilderBuilder PatternСтроитель. В некоторых контекстах встречается как Fluent Builder, если сочетается с цепочками методов.

Проблема, которую решает

При создании сложных объектов с большим количеством параметров (особенно необязательных) разработчики часто сталкиваются с несколькими проблемами:

  • Телескопические конструкторы – приходится создавать множество перегруженных конструкторов с разными наборами параметров.

  • Нечитаемый код – вызов конструктора с десятком параметров, где легко перепутать порядок.

  • Невозможность задать только нужные параметры – если объект имеет 20 полей, а нужны только 3, приходится передавать null или значения по умолчанию.

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

Суть паттерна

Builder предлагает вынести процесс конструирования объекта в отдельный класс (билдер), который поэтапно принимает значения параметров и в конце создаёт целевой объект. Это позволяет:

  • Создавать объекты с любым набором параметров в читаемой форме.

  • Изолировать сложную логику создания.

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

Классическая схема GoF включает Director, который управляет процессом построения, но в тестовой автоматизации чаще используется упрощённый вариант: билдер с методами для установки параметров и методом build().

Реализация

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

Рассмотрим класс User с несколькими полями.

Java (классический Builder с внутренним статическим классом)

public class User {
    private final String username;
    private final String password;
    private final String email;
    private final boolean isLocked;
    private final int loginAttempts;

    private User(Builder builder) {
        this.username = builder.username;
        this.password = builder.password;
        this.email = builder.email;
        this.isLocked = builder.isLocked;
        this.loginAttempts = builder.loginAttempts;
    }

    public static class Builder {
        private String username;
        private String password;
        private String email;
        private boolean isLocked = false;
        private int loginAttempts = 0;

        public Builder withUsername(String username) {
            this.username = username;
            return this;
        }

        public Builder withPassword(String password) {
            this.password = password;
            return this;
        }

        public Builder withEmail(String email) {
            this.email = email;
            return this;
        }

        public Builder locked(boolean isLocked) {
            this.isLocked = isLocked;
            return this;
        }

        public Builder loginAttempts(int attempts) {
            this.loginAttempts = attempts;
            return this;
        }

        public User build() {
            // Можно добавить валидацию
            if (username == null || password == null) {
                throw new IllegalStateException("Username and password are required");
            }
            return new User(this);
        }
    }
}

// Использование
User user = new User.Builder()
    .withUsername("john_doe")
    .withPassword("secret")
    .withEmail("john@example.com")
    .locked(true)
    .build();

TypeScript

class User {
    constructor(
        public readonly username: string,
        public readonly password: string,
        public readonly email?: string,
        public readonly isLocked: boolean = false,
        public readonly loginAttempts: number = 0
    ) {}
}

class UserBuilder {
    private username: string = '';
    private password: string = '';
    private email?: string;
    private isLocked: boolean = false;
    private loginAttempts: number = 0;

    withUsername(username: string): this {
        this.username = username;
        return this;
    }

    withPassword(password: string): this {
        this.password = password;
        return this;
    }

    withEmail(email: string): this {
        this.email = email;
        return this;
    }

    locked(isLocked: boolean): this {
        this.isLocked = isLocked;
        return this;
    }

    withLoginAttempts(attempts: number): this {
        this.loginAttempts = attempts;
        return this;
    }

    build(): User {
        if (!this.username || !this.password) {
            throw new Error('Username and password are required');
        }
        return new User(
            this.username,
            this.password,
            this.email,
            this.isLocked,
            this.loginAttempts
        );
    }
}

Python (с использованием dataclasses и именованных параметров)

from dataclasses import dataclass
from typing import Optional

@dataclass
class User:
    username: str
    password: str
    email: Optional[str] = None
    is_locked: bool = False
    login_attempts: int = 0

class UserBuilder:
    def __init__(self):
        self._username = None
        self._password = None
        self._email = None
        self._is_locked = False
        self._login_attempts = 0

    def with_username(self, username: str) -> 'UserBuilder':
        self._username = username
        return self

    def with_password(self, password: str) -> 'UserBuilder':
        self._password = password
        return self

    def with_email(self, email: str) -> 'UserBuilder':
        self._email = email
        return self

    def locked(self, is_locked: bool = True) -> 'UserBuilder':
        self._is_locked = is_locked
        return self

    def with_login_attempts(self, attempts: int) -> 'UserBuilder':
        self._login_attempts = attempts
        return self

    def build(self) -> User:
        if not self._username or not self._password:
            raise ValueError("Username and password are required")
        return User(
            username=self._username,
            password=self._password,
            email=self._email,
            is_locked=self._is_locked,
            login_attempts=self._login_attempts
        )

2. Builder для формирования API-запросов (Request Builder)

В API-тестах часто нужно строить сложные запросы с множеством параметров, фильтров, сортировок. Builder идеально подходит для этого.

Python

class SearchPartsRequestBuilder:
    def __init__(self):
        self.params = {}

    def with_vehicle(self, brand: str, model: str, generation: str = None) -> 'SearchPartsRequestBuilder':
        self.params['brand'] = brand
        self.params['model'] = model
        if generation:
            self.params['generation'] = generation
        return self

    def with_category(self, category: str) -> 'SearchPartsRequestBuilder':
        self.params['category'] = category
        return self

    def with_page(self, page: int, size: int = 20) -> 'SearchPartsRequestBuilder':
        self.params['page'] = page
        self.params['size'] = size
        return self

    def build(self) -> dict:
        return {k: v for k, v in self.params.items() if v is not None}

Java (с использованием RestAssured)

public class SearchPartsRequestBuilder {
    private Map<String, Object> params = new HashMap<>();

    public SearchPartsRequestBuilder withVehicle(String brand, String model, String generation) {
        params.put("brand", brand);
        params.put("model", model);
        if (generation != null) {
            params.put("generation", generation);
        }
        return this;
    }

    public SearchPartsRequestBuilder withCategory(String category) {
        params.put("category", category);
        return this;
    }

    public SearchPartsRequestBuilder withPagination(int page, int size) {
        params.put("page", page);
        params.put("size", size);
        return this;
    }

    public RequestSpecification build() {
        return RestAssured.given()
                .queryParams(params);
    }
}

TypeScript (axios)

class SearchPartsRequestBuilder {
    private params: Record<string, any> = {};

    withVehicle(brand: string, model: string, generation?: string): this {
        this.params.brand = brand;
        this.params.model = model;
        if (generation) this.params.generation = generation;
        return this;
    }

    withCategory(category: string): this {
        this.params.category = category;
        return this;
    }

    withPagination(page: number, size: number = 20): this {
        this.params.page = page;
        this.params.size = size;
        return this;
    }

    build(): Record<string, any> {
        return this.params;
    }
}

Вариативность реализации

  1. Классический GoF Builder – включает Director, который управляет последовательностью шагов. В тестах используется редко, но может пригодиться, если нужно гарантировать определённый порядок построения.

  2. Fluent Builder – самый популярный вариант, где методы возвращают this (реализует Fluent Interface).

  3. Статический внутренний класс (Java) – классическая реализация Effective Java (Блох). Позволяет создавать неизменяемые объекты с читаемым синтаксисом.

  4. Билдер с валидацией – в методе build() можно проверять обязательные поля и бизнес-правила.

  5. Аннотационный Builder (Lombok) – в Java библиотека Lombok позволяет генерировать билдер автоматически с помощью аннотации @Builder. Это сильно сокращает boilerplate-код.

  6. Именованные параметры с дефолтами (Python, Kotlin) – в языках с поддержкой именованных параметров и значений по умолчанию (Python через dataclass или pydantic, Kotlin data class) билдер становится менее критичным, но всё равно полезен для сложных случаев.

  7. Билдер для создания цепочек тестовых данных – можно комбинировать с Factory для создания предустановленных объектов.

Аннотационный Builder (Lombok) 

@Builder
public class User {
    private String username;
    private String password;
    // ...
}
// Использование: User user = User.builder().username("john").password("secret").build();

Когда применять, а когда нет

  • Применять:

    • Когда объект имеет более 3-4 параметров, особенно с необязательными.

    • Когда нужно создавать множество вариаций объекта (разные комбинации полей).

    • Когда процесс создания должен быть отделён от самого объекта (например, объект должен быть неизменяемым).

    • В API-тестах для построения сложных запросов (параметры, фильтры).

  • Не применять:

    • Для простых объектов с 1-2 параметрами (избыточно, лучше использовать конструктор или фабрику).

    • Если объект создаётся всегда одинаково (достаточно конструктора).

    • В высоконагруженных местах, где важна производительность (создание билдера и вызов методов даёт небольшой оверхед, но для тестов это некритично).

Связь с другими паттернами

  • Fluent Interface – почти всегда Builder реализуется через Fluent Interface, но это необязательно (можно использовать обычные сеттеры и в конце вызвать build()).

  • Factory – Factory может использовать Builder для создания сложных объектов, скрывая детали построения.

  • Prototype – Builder может создавать копии объектов с изменениями.

  • Composite – Builder может использоваться для построения древовидных структур (например, сложных JSON-объектов).

  • Immutable – Builder часто применяется для создания неизменяемых объектов.

Пример кода (расширенный с валидацией и кастомными шагами)

Java с Lombok и валидацией в билдере (через аннотацию)

import lombok.Builder;
import lombok.Singular;
import java.util.List;

@Builder
public class Order {
    private String customerName;
    @Singular private List<String> items;
    private double total;
    private boolean paid;

    // Кастомный билдер (Lombok сгенерирует, но можно дополнить)
    public static class OrderBuilder {
        public OrderBuilder validate() {
            if (customerName == null || customerName.isEmpty()) {
                throw new IllegalStateException("Customer name required");
            }
            if (items.isEmpty()) {
                throw new IllegalStateException("Order must have at least one item");
            }
            return this;
        }
    }
}

// Использование
Order order = Order.builder()
    .customerName("John Doe")
    .item("Laptop")
    .item("Mouse")
    .total(1500.0)
    .build();

Резюме

Builder Pattern – незаменимый инструмент для создания сложных объектов в автоматизации тестирования. Он повышает читаемость кода, упрощает поддержку при изменении структуры объектов и позволяет гибко комбинировать параметры. Особенно полезен при построении запросов к API и генерации тестовых данных. В сочетании с Fluent Interface и Factory даёт мощный и выразительный способ подготовки данных для тестов.

6. Factory / Object Mother

Название и синонимы

FactoryObject MotherTest Data FactoryТестовая фабрика. Иногда называют Test Data Builder в сочетании с паттерном Builder, но это разные вещи.

Проблема, которую решает

При написании тестов часто требуются одни и те же наборы данных: стандартный пользователь, пользователь с заблокированным аккаунтом, заказ с определённым составом, запрос с типовыми параметрами. Если эти данные создавать непосредственно в каждом тесте, возникает ряд проблем:

  • Дублирование кода – один и тот же код создания объекта повторяется в десятках тестов.

  • Сложность поддержки – при изменении структуры данных (например, добавлении нового обязательного поля) приходится править все места создания.

  • Несогласованность – разные тесты могут использовать немного разные данные, что затрудняет отладку.

  • Снижение читаемости – тест загромождается деталями создания, вместо того чтобы сразу перейти к сути проверки.

Суть паттерна

Factory / Object Mother предлагает вынести создание часто используемых тестовых объектов в отдельный класс (или набор методов), который предоставляет готовые экземпляры с предопределёнными (обычно валидными) данными. Тесты просто вызывают фабричный метод и получают готовый к использованию объект.

Различают два близких подхода:

  • Object Mother – содержит методы, возвращающие конкретные предопределённые объекты (например, standardUser()lockedUser()). Это простой и понятный способ.

  • Test Data Factory – может быть более гибкой: принимать параметры для изменения отдельных полей, комбинироваться с Builder'ом, генерировать уникальные данные через Faker.

В обоих случаях цель одна – централизовать создание тестовых данных и сделать тесты чище.

Реализация

1. Простая фабрика для пользователей (UI)

Рассмотрим пример.

Java (Object Mother)

public class UserFactory {
    public static User standardUser() {
        return new User.Builder()
                .withUsername("standard_user")
                .withPassword("secret_sauce")
                .build();
    }

    public static User lockedUser() {
        return new User.Builder()
                .withUsername("locked_out_user")
                .withPassword("secret_sauce")
                .locked(true)
                .build();
    }

    public static User problemUser() {
        return new User.Builder()
                .withUsername("problem_user")
                .withPassword("secret_sauce")
                .build();
    }
}

TypeScript

import { UserBuilder } from './UserBuilder';

export class UserFactory {
    static standardUser() {
        return new UserBuilder()
            .withUsername('standard_user')
            .withPassword('secret_sauce')
            .build();
    }

    static lockedUser() {
        return new UserBuilder()
            .withUsername('locked_out_user')
            .withPassword('secret_sauce')
            .locked(true)
            .build();
    }

    static problemUser() {
        return new UserBuilder()
            .withUsername('problem_user')
            .withPassword('secret_sauce')
            .build();
    }
}

Python

from .user_builder import UserBuilder

class UserFactory:
    @staticmethod
    def standard_user():
        return (UserBuilder()
                .with_username("standard_user")
                .with_password("secret_sauce")
                .build())

    @staticmethod
    def locked_user():
        return (UserBuilder()
                .with_username("locked_out_user")
                .with_password("secret_sauce")
                .locked()
                .build())

    @staticmethod
    def problem_user():
        return (UserBuilder()
                .with_username("problem_user")
                .with_password("secret_sauce")
                .build())

2. Комбинация с Builder (гибкая фабрика)

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

Java

public class UserFactory {
    public static UserBuilder standardUserBuilder() {
        return new UserBuilder()
                .withUsername("standard_user")
                .withPassword("secret_sauce");
    }
}

// В тесте:
User user = UserFactory.standardUserBuilder()
                .withEmail("custom@example.com")
                .build();

Вариативность реализации

  1. Object Mother (жестко заданные объекты) – самый простой и понятный подход. Подходит, когда нужно несколько фиксированных вариантов.

  2. Параметризованная фабрика – методы принимают аргументы для настройки возвращаемого объекта (например, createUser(String type)). Может быть удобно, но снижает читаемость.

  3. Комбинация с Builder – фабрика возвращает частично настроенный билдер, позволяя тесту донастроить объект.

  4. Использование Faker для уникальных данных – фабрика может генерировать каждый раз разные, но реалистичные данные, избегая коллизий при параллельном запуске.

  5. Фабрика для запросов с дефолтными параметрами – в API-тестах часто нужны запросы с типовыми, но валидными параметрами. Фабрика предоставляет такие "шаблоны".

  6. Иерархия фабрик – можно создавать специализированные фабрики для разных сущностей (UserFactory, OrderFactory, PartRequestFactory).

Пример кода (расширенный с Faker)

Python с Faker

from faker import Faker
from .user_builder import UserBuilder

fake = Faker()

class UserFactory:
    @staticmethod
    def standard_user():
        return UserBuilder().with_username("standard_user").with_password("secret_sauce").build()

    @staticmethod
    def random_user():
        return UserBuilder() \
            .with_username(fake.user_name()) \
            .with_password(fake.password()) \
            .with_email(fake.email()) \
            .build()

Резюме

Factory / Object Mother – ключевой паттерн для управления тестовыми данными. Он устраняет дублирование, централизует логику создания объектов и повышает читаемость тестов. В сочетании с Builder и Faker даёт мощный инструмент для генерации как фиксированных, так и уникальных данных.

7. Dependency Injection (DI) в тестировании

Название и синонимы

Dependency Injection (DI)Внедрение зависимостей. В контексте тестовых фреймворков часто говорят о фикстурах (fixtures) (pytest), хуках (hooks) или контексте. В Java-мире это также IoC-контейнеры (Spring, Guice, Dagger). В TypeScript/JavaScript – DI-контейнеры (InversifyJS, TSyringe) или просто передача объектов через аргументы.

Проблема, которую решает

В автоматизированных тестах часто требуется создавать и настраивать множество объектов: HTTP-клиенты, подключения к БД, Page Objects, моки, тестовые данные. Без DI каждый тест или тестовый класс сам отвечает за создание этих объектов. Это приводит к:

  • Дублированию кода – инициализация клиентов повторяется в каждом тесте или модуле.

  • Сложности поддержки – при изменении способа создания (например, добавлении таймаута в клиент) нужно править все места.

  • Плохой изоляции – объекты могут "жить" дольше одного теста, сохраняя состояние и влияя на другие тесты.

  • Затруднённому тестированию – трудно подменить реальный сервис моком, если клиент создаётся внутри теста жёстко.

  • Неэффективному использованию ресурсов – тяжёлые объекты (например, HTTP-клиент) могут создаваться многократно, хотя достаточно одного экземпляра.

Суть паттерна

Dependency Injection – это принцип, при котором объект получает свои зависимости извне, а не создаёт их сам. В тестировании это означает, что тесты не создают нужные им объекты, а получают их уже готовыми – через параметры, фикстуры, конструкторы или специальные контейнеры. Это позволяет:

  • Централизованно управлять созданием и жизненным циклом объектов.

  • Легко заменять реальные зависимости на моки или стабы.

  • Обеспечивать изоляцию тестов (каждый тест получает чистые объекты).

  • Переиспользовать тяжёлые объекты между тестами для экономии времени.

В современных тестовых фреймворках (pytest, Playwright, JUnit5) DI встроен в виде фикстур, которые автоматически передаются в тесты по запросу.

Реализация

1. Dependency Injection через фикстуры в Python (pytest)

В pytest фикстуры – это основной механизм DI. Фикстура определяет, как создать объект, и может иметь разную область видимости (function, class, module, session). Тест просто указывает фикстуру в аргументах и получает готовый объект.

Пример из API

import pytest
import httpx
from tests.api_clients.parts_client import PartsAPIClient

@pytest.fixture(scope="session")
async def http_client():
    """Создаёт HTTP-клиент на всю сессию тестов."""
    async with httpx.AsyncClient(base_url="http://localhost:8000") as client:
        yield client

@pytest.fixture
async def api_client(http_client):
    """Внедряет http_client в PartsAPIClient."""
    return PartsAPIClient(http_client)

@pytest.mark.asyncio
async def test_search_parts(api_client):
    response = await api_client.search_parts("BMW", "X5")
    assert response.status_code == 200

Здесь api_client зависит от http_client, и pytest автоматически разрешает зависимости, создавая объекты в правильном порядке.

2. Dependency Injection через фикстуры в Playwright (TypeScript)

Playwright предоставляет механизм фикстур, аналогичный pytest. Можно объявлять собственные фикстуры, которые будут передаваться в тесты.

Пример из UI

// fixtures.ts
import { test as base } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { InventoryPage } from '../pages/InventoryPage';
import { UserFactory } from '../data/UserFactory';

type MyFixtures = {
    loginPage: LoginPage;
    inventoryPage: InventoryPage;
    standardUser: User;
};

export const test = base.extend<MyFixtures>({
    loginPage: async ({ page }, use) => {
        const loginPage = new LoginPage(page);
        await loginPage.open();
        await use(loginPage);
    },
    inventoryPage: async ({ page }, use) => {
        const inventoryPage = new InventoryPage(page);
        await use(inventoryPage);
    },
    standardUser: async ({}, use) => {
        const user = UserFactory.standardUser();
        await use(user);
    }
});

export { expect } from '@playwright/test';

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

import { test, expect } from '../fixtures';

test('Успешная авторизация', async ({ loginPage, standardUser }) => {
    await loginPage.login(standardUser.username, standardUser.password);
    await expect(loginPage.page).toHaveURL(/inventory/);
});

3. Dependency Injection в Java (JUnit5 + Spring / Guice)

В Java можно использовать DI-контейнеры, такие как Spring или Guice, вместе с JUnit5. Например, Spring предоставляет @Autowired для внедрения бинов, а JUnit5 поддерживает расширения для DI.

Пример с Spring

@SpringBootTest
@AutoConfigureMockMvc
class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private UserRepository userRepository;

    @Test
    void testGetUser() throws Exception {
        userRepository.save(new User("john"));
        mockMvc.perform(get("/users/john"))
               .andExpect(status().isOk());
    }
}

Пример с Guice (JUnit5 + GuiceExtension)

@ExtendWith(GuiceExtension.class)
@GuiceModules(TestModule.class)
public class UserServiceTest {

    @Inject
    private UserService userService;

    @Inject
    private UserRepository userRepository;

    @Test
    void testCreateUser() {
        // ...
    }
}

Можно также использовать встроенные возможности JUnit5 по инъекции параметров тестов через TestInfoRepetitionInfo и т.д., но для пользовательских объектов обычно применяют расширения.

Вариативность реализации

  1. Встроенные фикстуры тестовых фреймворков (pytest, Playwright) – самый распространённый и удобный способ в Python и JS. Они поддерживают скоупы, автоматическую очистку и разрешение зависимостей.

  2. DI-контейнеры (Spring, Guice, Dagger, Inversify) – дают мощные возможности, но требуют настройки и могут быть избыточны для небольших проектов. Хороши в крупных корпоративных Java-проектах.

  3. Ручная инъекция через конструкторы/методы – классический DI, когда объекты создаются явно в setUp и передаются в тесты. Это просто и понятно, но может привести к дублированию кода.

  4. Контекстные объекты – создание единого объекта контекста, который содержит все зависимости, и передача его в тесты. Упрощает добавление новых зависимостей.

  5. Service Locator – антипаттерн, близкий к DI, но зависимости запрашиваются явно через глобальный реестр. В тестах может применяться, но считается менее предпочтительным из-за скрытых зависимостей.

Ручная инъекция через конструкторы/методы (Java)

public class MyTest {
    private ApiClient client;
    private UserService userService;

    @BeforeEach
    void setUp() {
        client = new ApiClient("http://localhost:8080");
        userService = new UserService(client);
    }

    @Test
    void test() {
        userService.doSomething();
    }
}

Контекстные объекты (TypeScript)

interface TestContext {
    apiClient: ApiClient;
    userService: UserService;
    loggedInUser: User;
}

beforeEach(() => {
    ctx = {
        apiClient: new ApiClient(),
        userService: new UserService(ctx.apiClient),
        loggedInUser: UserFactory.standardUser()
    };
});

test('something', () => {
    ctx.userService.doSomething();
});

Когда применять, а когда нет

  • Применять всегда в сколько-нибудь сложных тестовых проектах. DI – это фундамент для поддерживаемой архитектуры тестов.

  • Особенно важен при необходимости мокирования, изоляции тестов и переиспользования тяжёлых ресурсов.

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

Связь с другими паттернами

  • Page Object / API Client – о��ъекты страниц и клиенты создаются и внедряются через DI.

  • Factory – фабрики могут использоваться внутри фикстур для создания объектов с нужными данными.

  • Singleton – DI может управлять синглтонами (например, скоуп session в фикстурах).

  • Proxy – DI может внедрять прокси-обёртки для логирования или метрик.

  • Builder – часто используется внутри фикстур для создания сложных объектов.

Пример кода (расширенный с разными скоупами)

Python (pytest) с разными скоупами фикстур

import pytest
import httpx

@pytest.fixture(scope="session")
def config():
    """Читает конфиг один раз за сессию."""
    return {"base_url": "http://localhost:8000", "timeout": 10}

@pytest.fixture(scope="session")
async def http_client(config):
    """Создаёт клиент на всю сессию (тяжёлый ресурс)."""
    async with httpx.AsyncClient(base_url=config["base_url"], timeout=config["timeout"]) as client:
        yield client

@pytest.fixture
async def api_client(http_client):
    """Клиент API – создаётся для каждой функции, но использует общий http_client."""
    return PartsAPIClient(http_client)

@pytest.fixture
async def authenticated_client(api_client, test_user):
    """Логинится и возвращает клиент с токеном."""
    await api_client.login(test_user)
    return api_client

Dependency Injection в Java (JUnit5)

В Java для DI в тестах используют три основных подхода:

1. Ручное создание в @BeforeEach

class UserServiceTest {
    private UserService userService;
    private UserRepository userRepo;

    @BeforeEach
    void setUp() {
        userRepo = new MockUserRepository();  // или реальный
        userService = new UserService(userRepo);
    }

    @Test
    void test() {
        userService.doSomething();
    }
}

✅ Просто, быстро, нет оверхеда

❌ Дублирование кода при повторении в разных тестах

2. Spring Boot Test (стандарт для Spring-проектов)

@SpringBootTest
class UserServiceTest {
    @Autowired
    private UserService userService;  // внедряется реальный бин

    @MockBean
    private UserRepository userRepo;  // мок

    @Test
    void test() {
        when(userRepo.findById(1)).thenReturn(new User("john"));
        assertEquals("john", userService.getUser(1).getName());
    }
}

✅ Мощно, есть мокирование, профили, скоупы

❌ Тяжеловесно (поднимается контекст Spring)

3. JUnit5 ParameterResolver (своё расширение)

class ApiClientExtension implements ParameterResolver {
    @Override
    public boolean supportsParameter(...) { 
        return type == ApiClient.class; 
    }
    @Override
    public Object resolveParameter(...) { 
        return new ApiClient(); 
    }
}

@ExtendWith(ApiClientExtension.class)
class ApiTest {
    @Test
    void test(ApiClient client) {  // параметр внедряется автоматически
        client.call();
    }
}

✅ Близко к фикстурам pytest

❌ Для каждого типа нужно писать резолвер

8. Custom Fixtures / Setup Hooks

Название и синонимы

Custom FixturesSetup HooksTest HooksBefore/After Hooks. В разных фреймворках называются по-разному: @BeforeEach/@AfterEach (JUnit), setup/teardown (unittest), фикстуры (pytest), хуки (Playwright, Cypress), beforeAll/afterAll и т.д.

Проблема, которую решает

В тестах часто встречаются повторяющиеся действия, которые нужно выполнить до или после теста:

  • Логин в приложение

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

  • Очистка БД после теста

  • Поднятие мок-сервера

  • Установка и сброс состояния окружения

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

Суть паттерна

Custom Fixtures / Setup Hooks – это механизм вынесения повторяющейся логики подготовки и очистки в отдельные функции, которые автоматически выполняются до и/или после тестов. Фреймворк тестирования гарантирует их вызов в нужный момент, а тесты остаются чистыми и сфокусированными на проверках.

Фикстуры могут иметь разную область видимости:

  • Для каждого теста (function) – выполняются перед каждым тестом и после него

  • Для класса/модуля (class/module) – один раз для всех тестов в классе/модуле

  • Для сессии (session) – один раз за весь прогон тестов

Реализация

1. Setup Hooks в Java (JUnit5)

Базовые хуки JUnit5

import org.junit.jupiter.api.*;

class ShoppingCartTest {

    @BeforeAll
    static void initAll() {
        // Выполняется один раз перед всеми тестами
        System.out.println("Подготовка тестового окружения");
    }

    @BeforeEach
    void init() {
        // Выполняется перед каждым тестом
        System.out.println("Логин перед тестом");
        login("standard_user", "secret_sauce");
    }

    @Test
    void testAddToCart() {
        // Тест добавляет товар в корзину
    }

    @Test
    void testRemoveFromCart() {
        // Тест удаляет товар из корзины
    }

    @AfterEach
    void tearDown() {
        // Выполняется после каждого теста
        System.out.println("Очистка корзины");
        clearCart();
    }

    @AfterAll
    static void tearDownAll() {
        // Выполняется один раз после всех тестов
        System.out.println("Закрытие соединений");
        closeBrowser();
    }
}

Кастомная фикстура через @RegisterExtension

import org.junit.jupiter.api.extension.*;

public class LoggedInUserExtension implements BeforeEachCallback, AfterEachCallback {
    
    private User loggedInUser;
    
    @Override
    public void beforeEach(ExtensionContext context) throws Exception {
        loggedInUser = UserFactory.standardUser();
        login(loggedInUser);
        System.out.println("Залогинились как: " + loggedInUser.getUsername());
    }
    
    @Override
    public void afterEach(ExtensionContext context) throws Exception {
        logout();
        System.out.println("Разлогинились");
    }
    
    public User getLoggedInUser() {
        return loggedInUser;
    }
}

// Использование
class CartTest {
    
    @RegisterExtension
    static LoggedInUserExtension loggedInUser = new LoggedInUserExtension();
    
    @Test
    void testCart() {
        User user = loggedInUser.getLoggedInUser(); // получаем данные пользователя
        // тест уже выполняется под залогиненным пользователем
    }
}

2. Фикстуры в Python (pytest)

Базовые фикстуры pytest

import pytest
from playwright.sync_api import Page
from pages.login_page import LoginPage
from data.user_factory import UserFactory

@pytest.fixture(scope="function")
def logged_in_page(page: Page) -> Page:
    """Фикстура: логинится и возвращает страницу после логина"""
    login_page = LoginPage(page)
    login_page.open()
    login_page.login("standard_user", "secret_sauce")
    yield page
    # Очистка после теста
    page.context.clear_cookies()

@pytest.fixture(scope="session")
def api_client():
    """Фикстура: создаёт HTTP-клиент один раз за сессию"""
    import httpx
    with httpx.AsyncClient(base_url="http://localhost:8000") as client:
        yield client

# Использование
def test_add_to_cart(logged_in_page):
    # Тест уже выполняется на странице после логина
    logged_in_page.locator(".inventory_item button").first.click()

def test_api_search_parts(api_client):
    response = api_client.get("/api/v1/parts/search?brand=BMW")
    assert response.status_code == 200

Фикстура с автозапуском (autouse)

@pytest.fixture(autouse=True)
def clean_db():
    """Очищает БД перед каждым тестом автоматически"""
    db.execute("TRUNCATE users CASCADE")
    yield
    # опционально: действия после теста

3. Хуки и фикстуры в Playwright (TypeScript)

Встроенные хуки Playwright

import { test, expect } from '@playwright/test';

test.describe('Корзина', () => {
  
  test.beforeAll(async () => {
    // Один раз перед всеми тестами в describe
    console.log('Запуск тестов корзины');
  });

  test.beforeEach(async ({ page }) => {
    // Перед каждым тестом
    await page.goto('https://www.saucedemo.com');
    await page.locator('#user-name').fill('standard_user');
    await page.locator('#password').fill('secret_sauce');
    await page.locator('#login-button').click();
  });

  test('Добавление товара', async ({ page }) => {
    await page.locator('.inventory_item button').first().click();
    await expect(page.locator('.shopping_cart_badge')).toHaveText('1');
  });

  test('Удаление товара', async ({ page }) => {
    // Уже залогинены благодаря beforeEach
    await page.locator('.inventory_item button').first().click();
    await page.locator('.shopping_cart_link').click();
    await page.locator('.cart_button').click();
    await expect(page.locator('.cart_item')).toHaveCount(0);
  });

  test.afterEach(async ({ page }) => {
    // После каждого теста
    await page.context().clearCookies();
  });
});

Кастомные фикстуры Playwright (расширение базовых фикстур)

// fixtures.ts
import { test as base } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { InventoryPage } from '../pages/InventoryPage';
import { UserFactory } from '../data/UserFactory';

type MyFixtures = {
  loginPage: LoginPage;
  inventoryPage: InventoryPage;
  loggedInPage: InventoryPage;  // страница после логина
};

export const test = base.extend<MyFixtures>({
  loginPage: async ({ page }, use) => {
    await use(new LoginPage(page));
  },
  
  inventoryPage: async ({ page }, use) => {
    await use(new InventoryPage(page));
  },
  
  loggedInPage: async ({ page }, use) => {
    // Подготовка: логинимся
    const loginPage = new LoginPage(page);
    await loginPage.open();
    await loginPage.login('standard_user', 'secret_sauce');
    
    // Передаём страницу в тест
    await use(new InventoryPage(page));
    
    // Очистка после теста
    await page.context().clearCookies();
  }
});

export { expect } from '@playwright/test';

// Использование в тесте
import { test } from './fixtures';

test('Тест с залогиненным пользователем', async ({ loggedInPage }) => {
  // Уже на странице товаров после логина
  await loggedInPage.addItemToCart('Sauce Labs Backpack');
});

4. Setup Hooks в REST Assured (Java) для API

import io.restassured.RestAssured;
import io.restassured.specification.RequestSpecification;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

class PartsApiTest {
    
    private static RequestSpecification spec;
    private String authToken;
    
    @BeforeAll
    static void setupSpec() {
        // Один раз для всех тестов
        RestAssured.baseURI = "http://localhost:8000";
        RestAssured.basePath = "/api/v1";
    }
    
    @BeforeEach
    void setupAuth() {
        // Перед каждым тестом получаем свежий токен
        authToken = getAuthToken("test_user", "password");
    }
    
    @Test
    void testSearchParts() {
        given()
            .header("Authorization", "Bearer " + authToken)
            .queryParam("brand", "BMW")
        .when()
            .get("/parts/search")
        .then()
            .statusCode(200);
    }
    
    private String getAuthToken(String username, String password) {
        return given()
            .body("{\"username\":\"" + username + "\", \"password\":\"" + password + "\"}")
        .when()
            .post("/auth/login")
            .jsonPath()
            .getString("token");
    }
}

Вариативность реализации

  1. Встроенные хуки фреймворка (@BeforeEach@BeforeAllbeforeEach/afterEachsetup/teardown) – базовый механизм, есть во всех фреймворках.

  2. Пользовательские фикстуры с внедрением зависимостей (pytest fixtures, Playwright custom fixtures) – самый мощный подход, когда фикстуры могут зависеть друг от друга и автоматически разрешать зависимости.

  3. Расширения/Extensions (JUnit5 ExtensionTestWatcher) – для более сложной логики, когда нужно перехватывать события жизненного цикла теста.

  4. Автоматические (autouse) фикстуры – выполняются для каждого теста без явного указания (полезно для очистки БД, сброса состояния).

  5. Контекстные менеджеры (Python) – фикстуры могут использовать yield для разделения setup и teardown логики.

  6. Параметризованные фикстуры – фикстуры, принимающие параметры для настройки поведения.

@pytest.fixture
def user(request):
    """Создаёт пользователя указанного типа"""
    user_type = request.param
    return UserFactory.create_user(user_type)

@pytest.mark.parametrize("user", ["standard", "locked", "problem"], indirect=True)
def test_login(user):
    # тест с разными типами пользователей

Когда применять, а когда нет

  • Применять всегда – это основа организации тестов. Любой повторяющийся код подготовки/очистки должен быть вынесен в фикстуры.

  • Особенно важно для:

    • Логина (UI и API)

    • Подготовки тестовых данных

    • Очистки БД (изоляция тестов)

    • Настройки окружения (моки, контейнеры)

    • Создания тяжёлых объектов (HTTP-клиенты, драйверы браузера)

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

Связь с другими паттернами

  • Dependency Injection – фикстуры в pytest и Playwright – это реализация DI для тестов.

  • Page Object / API Client – фикстуры создают и внедряют объекты страниц и клиентов.

  • Factory – внутри фикстур часто используются фабрики для создания тестовых данных.

  • Builder – фикстуры могут использовать билдеры для настройки объектов.

  • Transactional Tests – фикстуры управляют транзакциями/очисткой данных.

Резюме

Custom Fixtures / Setup Hooks – это фундаментальный механизм организации тестового кода. Они позволяют вынести повторяющуюся логику подготовки и очистки, делая тесты чистыми, понятными и изолированными. В сочетании с DI (фикстурами pytest/Playwright) этот паттерн становится основой для построения масштабируемых тестовых фреймворков. Использование фикстур с разными скоупами (function, class, session) позволяет оптимизировать производительность тестов, не жертвуя изоляцией.

Часть 2. Паттерны управления данными

(генерация, подготовка и изоляция данных)

9. Test Data Factory с Faker (генерация реалистичных данных)

Название и синонимы

FakerTest Data Factory с FakerГенерация реалистичных тестовых данныхПсевдо-рандомные данные. В Java также известна библиотека JavaFaker, в JavaScript – Faker.js, в Python – Faker.

Проблема, которую решает

При написании тестов часто требуются данные, которые должны быть:

  • Разнообразными – чтобы покрыть разные сценарии

  • Валидными – соответствующими формату (email, телефон, VIN-номер)

  • Уникальными – чтобы избежать конфликтов при параллельном запуске

  • Реалистичными – близкими к реальным данным в продакшене

Если использовать жестко закодированные данные ("test@test.com", "Test User 1"), возникают проблемы:

  • Коллизии – несколько тестов пытаются создать пользователя с одинаковым email

  • Недостаточное покрытие – тесты не проверяют обработку разных форматов (например, email с точкой, длинные имена)

  • Несоответствие реальности – на проде данные выглядят иначе, и баги могут не проявиться

  • Скучные тесты – однотипные данные не дают уверенности в корректной работе системы

Суть паттерна

Test Data Factory с Faker – это использование библиотек-генераторов фейковых данных (Faker) для создания реалистичных, валидных и разнообразных тестовых данных. Фабрика (Factory) комбинируется с Faker, предоставляя методы для генерации объектов (пользователей, заказов, товаров) со случайными, но правдоподобными значениями.

Преимущества такого подхода:

  • Уникальность – каждый вызов генерирует разные данные (если не зафиксировать seed)

  • Валидность – Faker знает форматы: email, телефон, адрес, VIN, IBAN и т.д.

  • Локализация – можно генерировать данные для разных стран (ru_RU, en_US, de_DE)

  • Реалистичность – имена, фамилии, адреса выглядят как настоящие

  • Контроль – при необходимости можно зафиксировать seed для воспроизводимости тестов

Реализация

1. Python (библиотека Faker)

from faker import Faker
from dataclasses import dataclass
from typing import Optional

fake = Faker('ru_RU')  # русская локализация

@dataclass
class User:
    username: str
    email: str
    password: str
    first_name: str
    last_name: str
    phone: str
    is_active: bool = True

class UserFactory:
    @staticmethod
    def generate() -> User:
        """Генерирует случайного пользователя"""
        first_name = fake.first_name()
        last_name = fake.last_name()
        return User(
            username=fake.user_name(),
            email=fake.email(),
            password=fake.password(length=12),
            first_name=first_name,
            last_name=last_name,
            phone=fake.phone_number()
        )
    
    @staticmethod
    def generate_batch(count: int) -> list[User]:
        """Генерирует несколько пользователей"""
        return [UserFactory.generate() for _ in range(count)]
    
    @staticmethod
    def generate_with_seed(seed: int) -> User:
        """Генерирует пользователя с фиксированным seed (воспроизводимо)"""
        fake.seed_instance(seed)
        return UserFactory.generate()

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

def test_user_registration(api_client):
    # Генерируем уникального пользователя для каждого теста
    user = UserFactory.generate()
    
    response = api_client.register_user(
        username=user.username,
        email=user.email,
        password=user.password
    )
    
    assert response.status_code == 201
    assert response.json()["email"] == user.email

Фабрика для авто-запчастей (VIN, OEM)

class PartFactory:
    @staticmethod
    def generate_vin() -> str:
        """Генерирует валидный VIN-номер"""
        return fake.bothify(text='???-####-?????', letters='ABCDEFGHJKLMNPRSTUVWXYZ')
    
    @staticmethod
    def generate_oem() -> str:
        """Генерирует OEM-номер запчасти"""
        return fake.bothify(text='??-####-??', letters='ABCDEFGHIJKLMNOPQRSTUVWXYZ')
    
    @staticmethod
    def generate_part() -> dict:
        return {
            "id": fake.random_int(min=1000, max=999999),
            "name": fake.word().capitalize() + " " + fake.word(),
            "manufacturer": fake.company(),
            "price": round(fake.random_number(digits=4) / 100, 2),
            "oem_number": PartFactory.generate_oem(),
            "description": fake.sentence()
        }

2. TypeScript/JavaScript (Faker.js)

import { faker } from '@faker-js/faker/locale/ru';  // русская локализация

export interface User {
    username: string;
    email: string;
    password: string;
    firstName: string;
    lastName: string;
    phone: string;
    isActive: boolean;
}

export class UserFactory {
    static generate(): User {
        const firstName = faker.person.firstName();
        const lastName = faker.person.lastName();
        
        return {
            username: faker.internet.userName({ firstName, lastName }),
            email: faker.internet.email({ firstName, lastName }),
            password: faker.internet.password({ length: 12 }),
            firstName,
            lastName,
            phone: faker.phone.number(),
            isActive: true
        };
    }
    
    static generateBatch(count: number): User[] {
        return Array.from({ length: count }, () => this.generate());
    }
    
    static generateWithSeed(seed: number): User {
        faker.seed(seed);
        return this.generate();
    }
}

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

import { test, expect } from '@playwright/test';
import { UserFactory } from './factories/UserFactory';

test('регистрация нового пользователя', async ({ request }) => {
    const user = UserFactory.generate();
    
    const response = await request.post('/api/users', {
        data: {
            username: user.username,
            email: user.email,
            password: user.password
        }
    });
    
    expect(response.ok()).toBeTruthy();
    const body = await response.json();
    expect(body.email).toBe(user.email);
});

Фабрика с фиксированным набором полей (комбинация с Builder)

import { UserBuilder } from './UserBuilder';
import { faker } from '@faker-js/faker';

export class UserFactory {
    static standardUser() {
        return new UserBuilder()
            .withUsername('standard_user')
            .withPassword('secret_sauce')
            .build();
    }
    
    static randomUser() {
        return new UserBuilder()
            .withUsername(faker.internet.userName())
            .withPassword(faker.internet.password())
            .withEmail(faker.internet.email())
            .withFirstName(faker.person.firstName())
            .withLastName(faker.person.lastName())
            .build();
    }
}

Вариативность реализации

  1. Чистый Faker – генерация отдельных значений прямо в тестах. Просто, но может привести к разбросанным вызовам.

  2. Factory + Faker – фабричные методы, возвращающие готовые объекты со случайными полями. Самый популярный подход.

  3. Builder + Faker – билдеры с предустановленными случайными значениями по умолчанию.

  4. Seed для воспроизводимости – фиксация seed'а, чтобы тесты с рандомными данными были детерминированными при отладке.

  5. Локализация – использование разных локалей для проверки интернационализации.

  6. Комбинация с Data-Driven Testing – генерация наборов данных для параметризованных тестов.

  7. Генерация граничных значений – можно комбинировать Faker с Hypothesis (Python) или jqwik (Java) для property-based testing.

Когда применять, а когда нет

  • Применять:

    • Всегда, когда нужны уникальные, валидные данные (регистрация, создание сущностей)

    • Для наполнения БД тестовыми данными (seed data)

    • Для нагрузочного тестирования (генерация большого объёма данных)

    • Для проверки обработки разных форматов (email, телефон, VIN)

    • В сочетании с паттернами Factory и Builder

  • Не применять:

    • Когда нужны строго определённые данные для конкретного сценария (например, тест на граничное значение "пустая строка")

    • Если API или UI ожидает данные из фиксированного набора (enum, справочник)

    • В тестах, где важна полная детерминированность (но seed решает эту проблему)

Связь с другими паттернами

  • Factory – основа, к которой добавляется Faker.

  • Builder – билдер с рандомными значениями по умолчанию даёт гибкость и читаемость.

  • Data-Driven Testing – Faker может генерировать данные для параметризации.

  • Property-Based Testing – Faker используется внутри property-based библиотек (Hypothesis, jqwik) для генерации входных данных.

  • Object Mother – расширение Object Mother случайными данными.

Пример кода (расширенный с локализацией и seed)

Python – продвинутая фабрика с поддержкой локализации

import pytest
from faker import Faker

class LocalizedUserFactory:
    _fakers = {}
    
    @classmethod
    def _get_faker(cls, locale: str = 'en_US'):
        if locale not in cls._fakers:
            cls._fakers[locale] = Faker(locale)
        return cls._fakers[locale]
    
    @classmethod
    def generate(cls, locale: str = 'en_US', seed: int = None):
        faker = cls._get_faker(locale)
        if seed:
            faker.seed_instance(seed)
        
        return {
            'first_name': faker.first_name(),
            'last_name': faker.last_name(),
            'email': faker.email(),
            'phone': faker.phone_number(),
            'address': faker.address().replace('\n', ', '),
            'locale': locale
        }

# Параметризованный тест с разными локалями
@pytest.mark.parametrize('locale', ['en_US', 'ru_RU', 'de_DE'])
def test_user_registration_with_locale(api_client, locale):
    user_data = LocalizedUserFactory.generate(locale=locale)
    response = api_client.post('/users', json=user_data)
    assert response.status_code == 201
    assert response.json()['email'] == user_data['email']

Резюме

Test Data Factory с Faker – это мощный паттерн, который решает проблему создания реалистичных, разнообразных и уникальных тестовых данных. Комбинируя Factory с библиотекой Faker, мы получаем:

  • Автоматическую генерацию валидных данных (email, телефон, VIN)

  • Уникальность данных для каждого теста (нет коллизий)

  • Возможность воспроизводимости через seed

  • Поддержку разных локалей и форматов

  • Чистый, читаемый код тестов

Этот паттерн особенно ценен в интеграционных и нагрузочных тестах, где требуется большой объём реалистичных данных, а также в CI-среде, где параллельный запуск тестов требует уникальности данных.

10. Property-Based Testing (генерация широкого спектра данных)

Название и синонимы

Property-Based Testing (PBT) , Свойство-ориентированное тестированиеГенеративное тестирование. В отличие от пример-ориентированного тестирования (example-based testing), где мы задаём конкретные входные данные и ожидаемый результат, property-based testing проверяет свойства (инварианты) , которые должны выполняться для широкого спектра входных данных.

Проблема, которую решает

Традиционные тесты проверяют систему на конкретных примерах: 2 + 2 = 4. Но что насчёт 0 + 0-1 + 1999999 + 1null + null? Разработчик не может предусмотреть все возможные комбинации вручную. В результате:

  • Граничные случаи остаются непроверенными – баги проявляются на неожиданных данных.

  • Тесты скудны – проверяют только то, что придумал автор.

  • Данные "застывают" – тесты всегда используют одни и те же значения, не адаптируясь к изменениям.

Property-Based Testing решает эту проблему, автоматически генерируя сотни и тысячи вариантов входных данных и проверяя, что определённое свойство (например, "сумма двух положительных чисел всегда положительна") выполняется для всех сгенерированных случаев.

Суть паттерна

Property-Based Testing – это подход, при котором тесты описывают не конкретные примеры, а свойства, которые должны быть истинны для всех возможных входных данных (в рамках определённых ограничений). Специальная библиотека (генератор) создаёт множество случайных входных данных и проверяет на них указанное свойство. Если свойство нарушается, библиотека пытается найти минимальный воспроизводимый пример (shrinking), который вызывает ошибку.

Ключевые понятия:

  • Свойство (Property) – утверждение о системе, которое должно выполняться всегда (например, "после добавления товара в корзину, количество товаров увеличивается на 1").

  • Генератор (Generator) – создаёт случайные данные указанного типа (целые числа, строки, структуры).

  • Сжатие (Shrinking) – при нахождении ошибки библиотека уменьшает входные данные до минимального набора, который всё ещё воспроизводит проблему (облегчая отладку).

Реализация

1. Python (библиотека Hypothesis)

Базовый пример: тестирование функции сложения

from hypothesis import given, strategies as st, assume
from hypothesis import settings

# Простое свойство: сумма двух чисел коммутативна
@given(st.integers(), st.integers())
def test_addition_commutative(a, b):
    assert a + b == b + a

# Свойство с предусловием (только положительные числа)
@given(st.integers(min_value=1), st.integers(min_value=1))
def test_positive_sum_positive(a, b):
    assert a + b > 0

Тестирование API-эндпоинта (поиск запчастей)

from hypothesis import given, strategies as st
import httpx

# Стратегии для генерации данных
valid_brands = st.sampled_from(["BMW", "Audi", "Mercedes", "Toyota", "Ford"])
valid_models = st.text(min_size=1, max_size=20).filter(lambda x: x.isalnum())
categories = st.one_of(st.none(), st.text(min_size=3, max_size=15))

@given(
    brand=valid_brands,
    model=valid_models,
    category=categories
)
def test_search_parts_never_crashes(brand, model, category):
    """Свойство: API поиска запчастей никогда не падает с 500 ошибкой
    при любых валидных входных данных (возвращает либо 200, либо 404, либо 422)"""
    response = httpx.get(
        "http://localhost:8000/api/v1/parts/search",
        params={"brand": brand, "model": model, "category": category}
    )
    # Любой код ответа, кроме 5xx, считается приемлемым
    assert response.status_code < 500, f"Server error for {brand}/{model}/{category}"

2. TypeScript/JavaScript (библиотека fast-check)

Тестирование API (с Playwright/axios)

import fc from 'fast-check';
import { request } from '@playwright/test';

test('API поиска не падает с 500 ошибкой', async () => {
    await fc.assert(
        fc.asyncProperty(
            fc.string({ minLength: 1, maxLength: 20 }), // brand
            fc.string({ minLength: 1, maxLength: 20 }), // model
            fc.option(fc.string({ minLength: 3, maxLength: 15 })), // category
            async (brand, model, category) => {
                const params: any = { brand, model };
                if (category) params.category = category;
                
                const response = await request.get('http://localhost:8000/api/v1/parts/search', {
                    params
                });
                
                // Свойство: сервер не возвращает 5xx ошибки
                return response.status() < 500;
            }
        ),
        { numRuns: 100 } // количество генераций
    );
});

3. Java (библиотека jqwik)

Тестирование API с RestAssured

import net.jqwik.api.*;
import net.jqwik.api.constraints.*;
import io.restassured.response.Response;
import static io.restassured.RestAssured.given;

class PartsApiPropertiesTest {
    
    @Property(tries = 100)
    void searchNeverReturnsServerError(
            @ForAll @AlphaChars @StringLength(min = 1, max = 20) String brand,
            @ForAll @AlphaChars @StringLength(min = 1, max = 20) String model,
            @ForAll @WithNull @AlphaChars @StringLength(min = 3, max = 15) String category
    ) {
        Response response = given()
            .queryParam("brand", brand)
            .queryParam("model", model)
            .queryParam("category", category)
            .get("http://localhost:8000/api/v1/parts/search");
        
        // Свойство: статус код меньше 500
        Assertions.assertThat(response.getStatusCode()).isLessThan(500);
    }
    
    @Provide
    Arbitrary<SearchRequest> validSearchRequests() {
        Arbitrary<String> brands = Arbitraries.of("BMW", "Audi", "Toyota", "Ford");
        Arbitrary<String> models = Arbitraries.strings().alpha().ofMinLength(1).ofMaxLength(20);
        Arbitrary<Integer> pages = Arbitraries.integers().between(1, 100);
        
        return Combinators.combine(brands, models, pages)
            .as((brand, model, page) -> new SearchRequest(brand, model, page));
    }
    
    @Property
    void paginationWorks(@ForAll("validSearchRequests") SearchRequest request) {
        Response response = given()
            .queryParam("brand", request.getBrand())
            .queryParam("model", request.getModel())
            .queryParam("page", request.getPage())
            .get("/api/parts/search");
        
        response.then().statusCode(200);
        int returnedItems = response.jsonPath().getList("parts").size();
        // Свойство: количество элементов не превышает размер страницы (20 по умолчанию)
        Assertions.assertThat(returnedItems).isLessThanOrEqualTo(20);
    }
}

Вариативность реализации

  1. Встроенные стратегии (generators) – библиотеки предоставляют готовые генераторы для примитивов, коллекций, строк, email, URL и т.д.

  2. Комбинирование стратегий – создание сложных структур из простых (записи, списки, кортежи).

  3. Фильтрация (filtering) – assume или filter для исключения нежелательных значений.

  4. Кастомные генераторы – для специфичных доменных объектов (VIN, OEM-номера).

  5. Сужение (shrinking) – автоматический поиск минимального примера при падении теста.

  6. Ограничение числа запусков – tries/numRuns для баланса между покрытием и временем выполнения.

  7. Пример-ориентированные тесты + PBT – гибридный подход: несколько конкретных примеров + property-based тесты для общего покрытия.

  8. Stateful PBT – тестирование последовательностей операций (например, работа с корзиной, стеком, базой данных).

Когда применять, а когда нет

  • Применять:

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

    • Для проверки инвариа��тов API (например, "пагинация возвращает не больше запрошенного количества")

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

    • Когда есть альтернативная реализация для сравнения (например, новая и старая версия)

    • Для тестирования парсеровсериализатороввалидаторов

  • Не применять:

    • Когда система недетерминирована (зависит от времени, случайности)

    • Когда трудно сформулировать инвариант (например, "интерфейс выглядит красиво")

    • Для простых сценариев, где 2-3 примера уже дают уверенность

    • Когда производительность критична (генерация тысяч тестов может быть медленной)

Связь с другими паттернами

  • Factory + Faker – Faker можно использовать как генератор в PBT, хотя специализированные библиотеки (Hypothesis, fast-check) предоставляют более мощные средства.

  • Data-Driven Testing – PBT можно рассматривать как автоматическую DDT с бесконечным набором данных.

  • Contract Testing – можно проверять контракты API property-based подходом.

  • Builder – билдеры могут использоваться для создания сложных структур внутри PBT.

Резюме

Property-Based Testing – это мощная техника, которая дополняет традиционные пример-ориентированные тесты. Вместо того чтобы придумывать конкретные случаи вручную, мы описываем свойства, которые должны выполняться для всех входных данных, и позволяем библиотеке генерировать тысячи вариантов. Это позволяет находить баги в граничных случаях, которые разработчик не предусмотрел. PBT особенно эффективно для тестирования API, функций валидации, математических операций и любых систем с чёткими инвариантами.

11. Data-Driven Testing (параметризация тестов)

Название и синонимы

Data-Driven Testing (DDT) , Параметризованные тестыParameterized TestsTable-Driven Testing. В разных фреймворках встречаются аннотации: @ParameterizedTest (JUnit5), @Test с @MethodSource/@CsvSourcetest.each (Playwright, Jest), @pytest.mark.parametrize (pytest).

Проблема, которую решает

Часто требуется проверить одну и ту же функциональность с разными входными данными и ожидаемыми результатами. Например:

  • Логин с валидными/невалидными учётными данными.

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

  • Проверка граничных значений поля ввода.

Без DDT пришлось бы либо писать отдельный тест для каждого набора данных (дублирование кода), либо помещать все проверки в один тест с циклом (тогда при первом же падении тест останавливается, и мы не видим результатов для остальных кейсов, а также отчётность становится нечитаемой).

Суть паттерна

Data-Driven Testing – это подход, при котором логика теста отделяется от тестовых данных. Один тест (тело) выполняется многократно с разными наборами данных, которые передаются из внешнего источника (массив, CSV, JSON, метод). Каждый набор данных считается отдельным тест-кейсом в отчёте выполнения, что позволяет:

  • Сократить дублирование кода.

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

  • Получать понятные отчёты: при падении видно, на каких именно данных упал тест.

  • Централизованно управлять тестовыми данными.

Реализация

1. Python (pytest)

Базовый пример с @pytest.mark.parametrize

import pytest

@pytest.mark.parametrize("username,password,expected", [
    ("standard_user", "secret_sauce", "Products"),
    ("locked_out_user", "secret_sauce", "Epic sadface"),
    ("problem_user", "secret_sauce", "Products"),
    ("", "", "Username is required"),
])
def test_login(username, password, expected, login_page):
    login_page.open()
    login_page.login(username, password)
    assert expected in login_page.get_error_message() or login_page.get_title()

Параметризация с несколькими аргументами и именами тестов

@pytest.mark.parametrize("brand,model,expected_count", [
    ("BMW", "X5", 1),
    ("Toyota", "Camry", 1),
    ("NonExistent", "Brand", 0),
], ids=["bmw_x5", "toyota_camry", "nonexistent"])
def test_search_parts(api_client, brand, model, expected_count):
    response = api_client.search_parts(brand, model)
    assert response.status_code == 200
    assert len(response.json()["parts"]) == expected_count

Параметризация фикстур (indirect)

@pytest.fixture
def user(request):
    user_type = request.param
    return UserFactory.create(user_type)

@pytest.mark.parametrize("user", ["standard", "locked", "problem"], indirect=True)
def test_user_login(user, login_page):
    login_page.login(user.username, user.password)
    # проверки...

2. TypeScript (Playwright)

test.each с массивом данных

import { test, expect } from '@playwright/test';

const loginData = [
  { username: 'standard_user', password: 'secret_sauce', expected: 'Products' },
  { username: 'locked_out_user', password: 'secret_sauce', expected: 'Epic sadface' },
  { username: 'problem_user', password: 'secret_sauce', expected: 'Products' },
  { username: '', password: '', expected: 'Username is required' }
];

test.describe('Login tests', () => {
  for (const data of loginData) {
    test(`Login with ${data.username || 'empty'}`, async ({ page }) => {
      const loginPage = new LoginPage(page);
      await loginPage.open();
      await loginPage.login(data.username, data.password);
      
      if (data.expected === 'Products') {
        await expect(page).toHaveURL(/inventory/);
      } else {
        await expect(page.locator('.error-message')).toContainText(data.expected);
      }
    });
  }
});

Более идиоматичный способ с test.describe и циклом
Playwright не имеет встроенного @parametrize, но можно использовать цикл для генерации тестов.

Использование test.step и динамическое создание тестов

interface SearchTestCase {
  brand: string;
  model: string;
  expectedCount: number;
}

const searchCases: SearchTestCase[] = [
  { brand: 'BMW', model: 'X5', expectedCount: 1 },
  { brand: 'Toyota', model: 'Camry', expectedCount: 1 },
  { brand: 'NonExistent', model: 'Brand', expectedCount: 0 },
];

searchCases.forEach(({ brand, model, expectedCount }) => {
  test(`search parts for ${brand} ${model}`, async ({ request }) => {
    const response = await request.get('/api/v1/parts/search', {
      params: { brand, model }
    });
    expect(response.ok()).toBeTruthy();
    const data = await response.json();
    expect(data.parts.length).toBe(expectedCount);
  });
});

3. Java (JUnit5)

@ParameterizedTest с @ValueSource (для одного аргумента)

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.assertTrue;

class StringTest {
    @ParameterizedTest
    @ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" })
    void palindromes(String candidate) {
        assertTrue(isPalindrome(candidate));
    }
}

@CsvSource (несколько аргументов)

@ParameterizedTest
@CsvSource({
    "standard_user, secret_sauce, Products",
    "locked_out_user, secret_sauce, Epic sadface",
    "'', '', Username is required"
})
void testLogin(String username, String password, String expected) {
    LoginPage loginPage = new LoginPage(driver);
    loginPage.open();
    loginPage.login(username, password);
    
    if ("Products".equals(expected)) {
        assertEquals("Products", loginPage.getTitle());
    } else {
        assertTrue(loginPage.getErrorMessage().contains(expected));
    }
}

Вариативность реализации

  1. Простые источники данных:

    • @ValueSource – для одного аргумента (строки, числа, enum).

    • @CsvSource – для табличных данных в виде CSV-строк.

    • @EnumSource – все значения перечисления.

  2. Внешние источники:

    • @CsvFileSource – чтение из CSV-файла.

    • @JsonSource (через пользовательские аннотации) – можно реализовать самостоятельно.

    • Файлы Excel, YAML – через кастомные провайдеры.

  3. Методы-провайдеры (@MethodSource) – наиболее гибкий способ, позволяет возвращать Stream аргументов, объекты, кортежи.

  4. Динамическое создание тестов в рантайме (например, через TestFactory в JUnit5) – для случаев, когда данные известны только во время выполнения.

  5. Параметризация фикстур (pytest indirect, JUnit5 @MethodSource может передавать данные в конструктор теста).

  6. Комбинация с Builder/Factory – внутри провайдера данных можно использовать фабрики и билдеры для создания сложных объектов.

  7. Использование внешних данных – чтение из JSON/CSV/YAML файлов позволяет менять тестовые данные без перекомпиляции.

Когда применять, а когда нет

  • Применять всегда, когда нужно проверить множество однотипных сценариев. Это основа для покрытия граничных случаев и регрессионного тестирования.

  • Особенно полезно для:

    • Позитивных и негативных сценариев одного и того же действия.

    • Проверки различных комбинаций входных параметров.

    • Кросс-браузерного тестирования (параметризация браузерами).

    • Тестирования API с разными фильтрами.

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

Связь с другими паттернами

  • Factory / Object Mother – провайдеры данных могут использовать фабрики для создания тестовых объектов.

  • Builder – данные могут строиться через билдеры внутри провайдера.

  • Faker – можно генерировать случайные данные в провайдерах для большей вариативности, но осторожно (тесты должны быть детерминированы). Лучше фиксировать seed.

  • Property-Based Testing – PBT можно рассматривать как продвинутую форму DDT, где данные генерируются автоматически, а не задаются вручную.

Пример кода (расширенный с CSV-файлом)

Java: чтение тестовых данных из CSV-файла

@ParameterizedTest
@CsvFileSource(resources = "/test-data/login-data.csv", numLinesToSkip = 1)
void testLoginFromCsv(String username, String password, String expectedResult) {
    // тест
}

Python: чтение из JSON

import json
import pytest

with open("test_data.json") as f:
    test_data = json.load(f)

@pytest.mark.parametrize("data", test_data, ids=lambda d: d["name"])
def test_from_json(data):
    username = data["username"]
    password = data["password"]
    expected = data["expected"]
    # тест

TypeScript: внешний JSON

import testData from './login-data.json';

testData.forEach(({ username, password, expected }) => {
    test(`login with ${username}`, async ({ page }) => {
        // тест
    });
});

Резюме

Data-Driven Testing – фундаментальный паттерн, позволяющий эффективно покрывать множество сценариев одним тестом. Он улучшает поддерживаемость (данные отделены от логики), читаемость (тест описывает один сценарий, но выполняется много раз) и информативность отчётов. В современных фреймворках параметризация встроена нативно, и её использование – признак грамотно построенного тестового набора. 

12. Transactional Tests / Clean State

Название и синонимы

Transactional TestsClean State PatternИзоляция тестовDatabase CleanupReseed Pattern. В Java часто ассоциируется с @Transactional в Spring Test, в Python – с фикстурами, выполняющими откат транзакций или очистку таблиц, в JavaScript – с хуками afterEach/afterAll для удаления созданных данных.

Проблема, которую решает

При тестировании сервисов, работающих с базой данных (или другим хранилищем с состоянием), тесты могут влиять друг на друга:

  • Один тест создаёт запись, а второй тест ожидает пустую таблицу.

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

  • Параллельный запуск тестов приводит к конфликтам (duplicate key, гонки данных).

  • После прохождения тестов в БД остаются "мусорные" данные, которые могут повлиять на следующие прогоны.

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

Суть паттерна

Transactional Tests / Clean State гарантирует, что каждый тест начинается с известного, чистого состояния базы данных (или другого хранилища) и не оставляет после себя следов. Это достигается двумя основными способами:

  1. Транзакционный подход – тест выполняется внутри транзакции, которая в конце откатывается (rollback). База данных не видит изменений, всё остаётся как было. Идеально для тестов, которые только читают или изменяют данные, но не требуют фиксации.

  2. Очистка данных (Cleanup) – после каждого теста (или перед ним) явно удаляются созданные записи, либо выполняется TRUNCATE затронутых таблиц. Подходит, когда транзакции нежелательны (например, тестируются сами транзакции) или когда используются несколько хранилищ.

Цель – полная изоляция тестов друг от друга, чтобы они могли запускаться в любом порядке, параллельно, и не зависеть от окружения.

Реализация

1. Java (Spring Framework)

В Spring Test модуль предоставляет мощную поддержку транзакционных тестов.

@Transactional на уровне теста

import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
import org.junit.jupiter.api.Test;

@SpringBootTest
@Transactional  // каждый тест будет выполняться в транзакции, которая откатится в конце
class UserServiceTest {

    @Autowired
    private UserRepository userRepository;

    @Test
    void testCreateUser() {
        User user = new User("john", "john@example.com");
        userRepository.save(user);
        
        // Проверки
        assertNotNull(user.getId());
    }
    
    // После теста транзакция откатится, пользователь не сохранится в БД
}

2. Python (pytest + SQLAlchemy / Django)

Фикстура с транзакцией (SQLAlchemy)

import pytest
from myapp.database import SessionLocal, engine
from sqlalchemy.orm import Session

@pytest.fixture
def db_session():
    """Создаёт сессию и транзакцию, после теста откатывает."""
    connection = engine.connect()
    transaction = connection.begin()
    session = Session(bind=connection)
    
    yield session
    
    session.close()
    transaction.rollback()
    connection.close()

def test_create_user(db_session):
    user = User(name="john", email="john@example.com")
    db_session.add(user)
    db_session.flush()  # получаем ID, но не коммитим
    assert user.id is not None
    # После теста откат, пользователь не сохранится

Фикстура с очисткой таблиц (TRUNCATE)

@pytest.fixture(autouse=True)
def clean_database():
    """Очищает все таблицы перед каждым тестом."""
    with engine.connect() as conn:
        conn.execute("TRUNCATE users, orders RESTART IDENTITY CASCADE")
        conn.commit()
    yield
    # После теста можно ничего не делать

3. JavaScript (TypeScript) с различными ORM

После каждого теста очистка через afterEach (например, с Prisma)

import { prisma } from './prisma-client';
import { afterEach, beforeEach, test, expect } from '@playwright/test'; // или jest

describe('User tests', () => {
    beforeEach(async () => {
        // Очищаем таблицы перед тестом
        await prisma.user.deleteMany();
    });
    
    afterEach(async () => {
        // Можно ещё раз очистить после
        await prisma.user.deleteMany();
    });
    
    test('create user', async () => {
        const user = await prisma.user.create({
            data: { email: 'test@example.com', name: 'Test' }
        });
        expect(user.id).toBeDefined();
    });
});

Использование транзакций с откатом (например, with testcontainers)

import { Client } from 'pg';

test('database test with transaction', async () => {
    const client = new Client({ connectionString: process.env.DATABASE_URL });
    await client.connect();
    await client.query('BEGIN');
    
    try {
        await client.query('INSERT INTO users (name) VALUES ($1)', ['john']);
        const res = await client.query('SELECT * FROM users');
        expect(res.rowCount).toBe(1);
    } finally {
        await client.query('ROLLBACK');
        await client.end();
    }
});

TypeORM + testcontainers (пример)

import { createConnection, getConnection } from 'typeorm';

beforeEach(async () => {
    await createConnection({
        type: 'postgres',
        url: process.env.TEST_DATABASE_URL,
        entities: [User],
        dropSchema: true,  // создаёт схему заново перед тестами
        synchronize: true,
    });
});

afterEach(async () => {
    await getConnection().close();
});

Вариативность реализации

  1. Транзакционный откат (Rollback) – самый быстрый и чистый способ, если приложение поддерживает транзакции и тесты не требуют COMMIT. Отлично подходит для unit-тестов репозиториев, сервисов.

  2. Очистка таблиц (TRUNCATE / DELETE) – универсальный способ, работает даже если транзакции не используются (например, тестируются сами транзакции). Может быть медленнее при больших объёмах данных.

  3. Пересоздание схемы (Schema recreation) – для тестов, где нужно гарантированно чистое состояние, можно перед всеми тестами пересоздавать структуру БД (drop and create tables). Используется в интеграционных тестах с тестовыми контейнерами.

  4. Изоляция через схемы/базы данных – каждому тесту можно выделять отдельную схему или базу (например, с помощью Docker-контейнеров на тест). Максимальная изоляция, но дорого по времени.

  5. Снимки (snapshots) базы данных – восстановление состояния из снимка перед каждым тестом (редко).

  6. Testcontainers – поднятие чистого контейнера с БД для каждого тестового модуля/класса.

Когда применять, а когда нет

  • Применять обязательно для всех тестов, которые работают с базой данных, файловой системой или любым другим хранилищем с состоянием. Это основа надёжности и изоляции.

  • Не применять, если тесты ничего не сохраняют и не меняют состояние (только чтение). Но даже тогда может быть полезно убедиться, что данные не изменились.

  • В некоторых случаях (например, тестирование E2E через UI) полная очистка БД между тестами может быть слишком медленной – тогда используют точечную очистку только тех данных, которые создаются в тесте.

Связь с другими паттернами

  • Dependency Injection – через DI можно внедрять транзакционные менеджеры или сессии, управляющие транзакциями.

  • Factory / Builder – фабрики создают объекты, которые затем сохраняются в БД и будут очищены.

  • Setup Hooks – фикстуры (beforeEachafterEach) естественным образом используются для реализации Clean State.

  • Test Data Factory – создание данных, которые потом будут удалены.

  • Parallel Execution – без Clean State параллельный запуск невозможен.

Резюме

Transactional Tests / Clean State – критически важный паттерн для обеспечения надёжности и изоляции тестов, работающих с базами данных. Без него тесты становятся хрупкими, зависимыми от порядка выполнения и непригодными для параллельного запуска. Реализация может варьироваться от отката транзакций до полной очистки таблиц, но цель одна: каждый тест видит базу в известном, чистом состоянии. 

13. Snapshot Testing (сравнение с эталоном)

Название и синонимы

Snapshot TestingGolden Master TestingApproval TestingТестирование снимками. В UI-контексте часто называют визуальное регрессионное тестирование (visual regression testing).

Проблема, которую решает

При изменении кода легко случайно изменить вывод системы: структуру JSON-ответа, внешний вид страницы, текст ошибки. Обычные тесты проверяют отдельные поля, но не гарантируют, что остальные части остались неизменными. В результате:

  • Можно пропустить побочные эффекты рефакторинга.

  • Трудно поддерживать множество точечных ассертов.

  • Сложно тестировать сложные структуры (JSON, HTML, скриншоты).

Суть паттерна

Snapshot Testing заключается в сохранении эталонного результата работы тестируемой функции (ответ API, скриншот страницы, сгенерированный файл) и сравнении с текущим результатом при последующих запусках. Если результаты отличаются, тест падает, и разработчик либо исправляет код, либо обновляет эталон (если изменение ожидаемое).

Снэпшоты особенно полезны для:

  • Полных ответов API (чтобы заметить добавление/удаление полей).

  • Визуального тестирования UI (скриншоты).

  • Тестирования генерации кода, конфигураций, сообщений.

Реализация

1. API: сравнение JSON-ответов

Python (pytest-snapshot)

def test_api_search_parts(snapshot, api_client):
    response = api_client.get("/parts/search", params={"brand": "BMW", "model": "X5"})
    assert response.status_code == 200
    snapshot.assert_match(response.json(), "bmw_x5_response.json")

При первом запуске создаётся файл snapshots/test_api_search_parts/bmw_x5_response.json. При последующих запусках содержимое сравнивается.

TypeScript (Jest)

test('API search parts snapshot', async () => {
    const response = await request.get('/parts/search?brand=BMW&model=X5');
    expect(response.data).toMatchSnapshot('bmw-x5-response');
});

Jest создаёт снэпшот в папке snapshots.

Java (ApprovalTests)

import org.approvaltests.Approvals;
import org.junit.jupiter.api.Test;

class ApiSnapshotTest {
    @Test
    void testSearchParts() {
        Response response = given()
            .queryParam("brand", "BMW")
            .queryParam("model", "X5")
            .get("/parts/search");
        
        Approvals.verify(response.asString());
    }
}

2. UI: визуальное тестирование (скриншоты)

TypeScript (Playwright)

test('inventory page screenshot', async ({ page }) => {
    await page.goto('https://www.saucedemo.com/inventory.html');
    await expect(page).toHaveScreenshot('inventory-page.png', {
        mask: [page.locator('.inventory_item_price')] // скрыть динамические цены
    });
});

Python (Playwright)

def test_inventory_page_screenshot(page):
    page.goto("https://www.saucedemo.com/inventory.html")
    page.locator(".inventory_item_price").evaluate("el => el.style.visibility = 'hidden'")
    page.screenshot(path="inventory-page.png")
    # сравнение с эталоном обычно через assert и предварительное сохранение

Java (Selenium + AssertJ)

@Test
void testScreenshot() throws IOException {
    driver.get("https://www.saucedemo.com/inventory.html");
    File screenshot = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
    // сравнение с эталоном через библиотеку (например, AssertJ)
}

Вариативность реализации

  1. Текстовые снэпшоты – JSON, HTML, текстовые файлы.

  2. Бинарные снэпшоты – изображения, PDF.

  3. Маскирование динамических данных – замена ID, дат, цен на константы перед сравнением.

  4. Интерактивное обновление – возможность обновить снэпшоты одной командой (--update-snapshots).

  5. Снэпшоты для разных окружений – отдельные папки для разных браузеров/ОС.

  6. Пороговое сравнение изображений – допуск небольших различий (например, из-за антиалиасинга).

Когда применять, а когда нет

  • Применять:

    • Для проверки полных ответов API (контрактное тестирование на практике).

    • Для визуального регрессионного тестирования UI.

    • При рефакторинге, чтобы убедиться, что вывод не изменился.

    • Для тестирования генераторов (кода, документации).

  • Не применять:

    • Для сильно динамических данных без маскирования.

    • Если эталоны обновляются слишком часто (теряется ценность).

    • Когда важна проверка конкретных бизнес-правил, а не всей структуры.

Связь с другими паттернами

  • Schema Validation – снэпшоты проверяют данные, а схема – только структуру.

  • Contract Testing – снэпшоты могут фиксировать контракт между сервисами.

  • Visual Testing – частный случай снэпшотов для UI.

  • Data-Driven Testing – можно параметризовать создание снэпшотов для разных входных данных

Резюме

Snapshot Testing – эффективный способ автоматически отслеживать изменения в выводимых данных. Он снижает количество точечных ассертов, ускоряет написание тестов и помогает обнаруживать неожиданные регрессии. Однако требует аккуратного обращения с динамическими полями и дисциплины при обновлении эталонов.

Часть 3. Паттерны обеспечения надёжности и стабильности

(борьба с flaky-тестами, race conditions, сбоями окружения)

14. Waiter / Polling Pattern (ожидания с retry и экспоненциальной задержкой)

Название и синонимы

Waiter PatternPolling PatternExplicit WaitRetry with BackoffConditional Wait. В UI-автоматизации известен как Explicit Waits (Selenium) или Auto-waiting (Playwright). В API-тестировании часто называют Polling Assert или Awaitility.

Проблема, которую решает

Современные приложения полны асинхронных операций: данные записываются в БД, отправляются события в Kafka, UI обновляется после AJAX-запросов. Тесты, которые проверяют результат сразу после действия, часто падают из-за race conditions: операция ещё не завершилась, а тест уже проверяет. Использование фиксированных пауз (Thread.sleep(5000)) делает тесты медленными и нестабильными (flaky) — если время выполнения варьируется, тест то падает, то проходит.

Суть паттерна

Waiter / Polling Pattern заключается в активном ожидании наступления некоторого условия. Вместо того чтобы ждать фиксированное время, тест периодически (с заданным интервалом) проверяет условие, пока оно не станет истинным или не истечёт таймаут. Для повышения эффективности интервал может увеличиваться экспоненциально (exponential backoff), чтобы не нагружать систему частыми проверками, когда условие ещё далеко от выполнения.

Этот паттерн применяется как в UI-тестах (ожидание появления элемента, изменения состоян��я), так и в API-тестах (ожидание записи в БД, обработки события в Kafka, изменения статуса внешнего сервиса).

Реализация

1. Java

UI: Selenium WebDriverWait

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.openqa.selenium.support.ui.ExpectedConditions;
import java.time.Duration;

public void waitForElementVisible(WebDriver driver, By locator) {
    WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
    wait.until(ExpectedConditions.visibilityOfElementLocated(locator));
}

API: Ожидание записи в БД (самописный Poller)

import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;

public class Poller {
    public static <T> T waitFor(Callable<T> condition, long timeout, long initialDelay, long maxDelay) 
            throws Exception {
        long start = System.currentTimeMillis();
        long delay = initialDelay;
        while (System.currentTimeMillis() - start < timeout) {
            T result = condition.call();
            if (result != null && !Boolean.FALSE.equals(result)) {
                return result;
            }
            Thread.sleep(delay);
            delay = Math.min(delay * 2, maxDelay); // exponential backoff
        }
        throw new TimeoutException("Condition not met within " + timeout + " ms");
    }
}

// Использование
User user = Poller.waitFor(() -> userRepository.findByEmail("test@example.com"), 
    10000, 500, 5000);
assertNotNull(user);

2. TypeScript (JavaScript)

UI: Playwright auto-waiting (встроенное)
Playwright автоматически ожидает элементы перед действиями, но для кастомных условий можно использовать expect.poll:

import { expect, test } from '@playwright/test';

test('example', async ({ page }) => {
    await page.goto('https://example.com');
    // Ожидание, пока текст не станет определённым
    await expect.poll(async () => {
        return page.locator('h1').textContent();
    }, { timeout: 10000, intervals: [1000, 2000, 4000] }).toBe('Expected Title');
});

API: Самописный poller с экспоненциальной задержкой

async function waitForCondition<T>(
    condition: () => Promise<T>,
    timeout: number = 10000,
    initialInterval: number = 500,
    maxInterval: number = 5000
): Promise<T> {
    const start = Date.now();
    let interval = initialInterval;
    while (Date.now() - start < timeout) {
        const result = await condition();
        if (result) return result;
        await new Promise(resolve => setTimeout(resolve, interval));
        interval = Math.min(interval * 2, maxInterval);
    }
    throw new Error(`Condition not met within ${timeout}ms`);
}

// Использование
const user = await waitForCondition(
    async () => await db.findUserByEmail('test@example.com'),
    10000
);

3. Python

UI: Selenium WebDriverWait

from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

def wait_for_element(driver, locator, timeout=10):
    return WebDriverWait(driver, timeout).until(
        EC.visibility_of_element_located(locator)
    )

UI: Playwright auto-waiting (встроенное)

Playwright автоматически ждёт, но для кастомных условий:

from playwright.sync_api import expect

def test_example(page):
    page.goto("https://example.com")
    expect(page.locator("h1")).to_have_text("Expected Title", timeout=10000)

Вариативность реализации

  1. Фиксированный интервал – простейший случай.

  2. Экспоненциальный backoff – снижает нагрузку при долгом ожидании.

  3. Комбинация с джиттером – добавляет случайность, чтобы избежать эффекта "Thundering herd".

  4. Использование встроенных механизмов фреймворков – Selenium ExpectedConditions, Playwright auto-waiting, которые уже реализуют polling.

  5. Специализированные библиотеки – Awaitility (Java), p-wait-for (JS), tenacity (Python).

  6. Ожидание нескольких условий – можно комбинировать через and/or.

  7. Ожидание с проверкой исключений – игнорировать определённые исключения во время опроса.

Когда применять, а когда нет

  • Применять обязательно для всех асинхронных операций:

    • Проверка появления элемента в UI после AJAX.

    • Проверка записи в БД после вызова API.

    • Проверка обработки события в Kafka/RabbitMQ.

    • Проверка изменения статуса внешнего сервиса.

  • Не применять только для синхронных операций, где результат доступен немедленно.

  • Важно: не заменять polling-ом синхронные проверки, это добавит ненужных задержек.

Связь с другими паттернами

  • Retry Pattern – близок, но retry чаще применяется к повторным вызовам API, а waiter – к ожиданию состояния.

  • Circuit Breaker – может быть комбинирован: если условие не выполняется долго, можно разомкнуть цепь и пропустить тест.

  • Explicit Wait – частный случай для UI.

Пример кода (расширенный с проверкой Kafka)

Python: Ожидание события в Kafka

import asyncio
from kafka import KafkaConsumer

async def wait_for_kafka_message(topic, expected_key, timeout=10):
    consumer = KafkaConsumer(topic, bootstrap_servers='localhost:9092', auto_offset_reset='earliest')
    start = time.time()
    interval = 0.5
    while time.time() - start < timeout:
        messages = consumer.poll(timeout_ms=1000)
        for tp, records in messages.items():
            for record in records:
                if record.key == expected_key:
                    return record.value
        await asyncio.sleep(interval)
        interval = min(interval * 2, 5)
    raise TimeoutError(f"Message with key {expected_key} not found in {topic}")

Резюме

Waiter / Polling Pattern – фундаментальный инструмент для борьбы с асинхронностью в тестах. Он делает тесты стабильными, заменяя «слепые» паузы интеллектуальным ожиданием с обратной связью. В современных фреймворках (Playwright, Selenium) он уже встроен, но для нестандартных условий (БД, очереди) необходимо реализовывать собственные поллеры или использовать специализированные библиотеки. Правильное применение этого паттерна повышает стабильность тестов с 80% до 99% и сокращает время прогона за счёт отказа от избыточных задержек.

15. Retry Pattern (автоматический перезапуск упавших тестов)

Название и синонимы

Retry PatternTest RetryFlaky Test RetryАвтоматический перезапуск тестов. В различных фреймворках реализуется через аннотации/конфигурации: @Retry (JUnit, TestNG), retries в Playwright, pytest-rerunfailures (pytest), flaky (Python).

Проблема, которую решает

В автоматизированных тестах иногда возникают ложные падения (flaky tests) , вызванные временными проблемами: сетевыми таймаутами, гонками состояний, нестабильностью окружения, задержками в асинхронных системах. Такие падения не связаны с реальными дефектами, но приводят к:

  • Ложным срабатываниям CI/CD, блокировке мержа.

  • Потере времени разработчиков на анализ несуществующих багов.

  • Снижению доверия к тестам.

Полностью устранить flaky-тесты невозможно, но можно смягчить их влияние, автоматически перезапуская упавшие тесты.

Суть паттерна

Retry Pattern заключается в автоматическом повторном запуске теста, если он завершился неудачей. Если повторный запуск проходит успешно, тест считается зелёным, а факт перезапуска логируется для последующего анализа. Это позволяет отсеять временные сбои, сохраняя при этом возможность обнаружить реальные проблемы (если тест падает при всех повторах).

Важно различать retry на уровне теста (перезапуск всего тестового метода) и retry на уровне операции (повтор запроса внутри теста). Retry Pattern обычно относится к первому, хотя второй также важен и может использоваться внутри тестов (см. Waiter Pattern).

Реализация

1. Python (pytest)

Использование маркера для отдельных тестов

import pytest

@pytest.mark.flaky(reruns=3, reruns_delay=2)
def test_unstable_feature():
    # тест, который иногда падает
    ...

В коде с настройкой в pytest.ini

[pytest]
addopts = --reruns 2 --reruns-delay 1

2. TypeScript (Playwright)

Настройка в playwright.config.ts

import { defineConfig } from '@playwright/test';

export default defineConfig({
  retries: 2,  // глобально для всех тестов
  // или для конкретного проекта
  projects: [
    {
      name: 'chromium',
      use: { ... },
      retries: 1,
    }
  ]
});

Для конкретного теста (через аннотацию)

test.describe('flaky tests', () => {
  test('should work eventually', {
    retries: 3
  }, async ({ page }) => {
    // тест
  });
});

3. Java (JUnit5)

В JUnit5 нет встроенного механизма retry, но можно использовать расширения, например, из библиотеки junit-pioneer или написать своё.

import org.junitpioneer.jupiter.Retry;

@Retry(3)
@Test
void flakyTest() {
    // тест будет повторён до 3 раз при падении
}

TestNG (встроенный retryAnalyzer)

public class MyRetryAnalyzer implements IRetryAnalyzer {
    private int retryCount = 0;
    private static final int maxRetryCount = 3;

    @Override
    public boolean retry(ITestResult result) {
        if (retryCount < maxRetryCount) {
            retryCount++;
            return true;
        }
        return false;
    }
}

@Test(retryAnalyzer = MyRetryAnalyzer.class)
public void flakyTest() {
    // ...
}

Вариативность реализации

  1. Глобальные retries – применяются ко всем тестам (полезно для нестабильного окружения).

  2. Выборочные retries – только для помеченных тестов (точнее).

  3. Retries с задержкой – между попытками можно добавить паузу, чтобы дать время системе успокоиться.

  4. Retries только для определённых исключений – например, только для TimeoutException, а для AssertionError не повторять.

  5. Логирование retries – запись факта перезапуска для анализа flakiness.

  6. Интеграция с CI – в отчётах помечать тесты, которые прошли после retry, как "flaky" или "PASSED (retried)".

Когда применять, а когда нет

  • Применять:

    • Для тестов, работающих с внешними сервисами (сеть, БД, API).

    • Для асинхронных сценариев, где возможны задержки (Kafka, очереди).

    • В CI-среде, где окружение может быть нестабильным.

    • Как временное решение для известных flaky-тестов, пока не найдена первопричина.

  • Не применять:

    • Для тестов, проверяющих критически важную логику, где любое падение должно быть исследовано (retry может скрыть реальный баг).

    • Если flaky-тесты не анализируются – retry маскирует проблему, но не решает её.

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

    • Если retry настроен с большим числом повторов без задержки, это может привести к ложным успехам.

Связь с другими паттернами

  • Waiter / Polling Pattern – retry на уровне теста, а waiter на уровне операции внутри теста. Часто комбинируются.

  • Circuit Breaker – если тесты постоянно падают, можно разомкнуть цепь и пропустить их.

  • Flaky Test Management – ретраи – это один из способов управления flaky-тестами.

Когда retry вреден: антипаттерны

  • Маскировка реальных багов – если тест всегда падает из-за дефекта, retry создаст иллюзию стабильности, пока однажды не исчерпает лимит.

  • Увеличение времени прогона – retry делает тесты медленнее, особенно если много повторов.

  • Игнорирование корневой причины – вместо исправления flaky-теста разработчики просто добавляют retry.

  • Retry для всех тестов подряд – неоправданно, лучше точечно.

Рекомендации

  • Анализируйте тесты, которые часто требуют retry, и исправляйте их.

  • Используйте retry с небольшим числом повторов (1-2) и задержкой.

  • В отчётах помечайте тесты, прошедшие после retry, чтобы отличать их от стабильных.

  • Комбинируйте с Waiter Pattern внутри тестов для устранения источников flakiness.

Резюме

Retry Pattern – полезный инструмент для повышения стабильности тестов в условиях временных сбоев. Он позволяет сократить ложные падения в CI и сэкономить время разработчиков. Однако его следует применять с осторожностью, не забывая анализировать причины flakiness и не скрывая реальные дефекты. Правильно настроенный retry – это баланс между надёжностью и достоверностью тестов.

16. Circuit Breaker Pattern

Название и синонимы

Circuit BreakerПредохранительАвтоматический выключатель. В тестировании иногда называют Health Check Pattern или Dependency Health Check.

Проблема, которую решает

В автоматизированных тестах, особенно в CI-среде, тесты часто зависят от внешних сервисов: API сторонних поставщиков, тестовых стендов, баз данных, очередей. Если такой сервис недоступен (например, ведутся работы или упал контейнер), все тесты, которые его используют, начинают падать с таймаутами или ошибками соединения. Это приводит к:

  • Пустой трате времени CI (тесты висят, ждут таймауты, потом падают).

  • Засорению отчётов множеством ошибок, из-за которых сложно увидеть реальные проблемы.

  • Демотивации команды — все тесты красные, хотя проблема не в коде.

Суть паттерна

Circuit Breaker — это механизм, который отслеживает состояние внешней зависимости и, если она недоступна, временно отключает (пропускает) тесты, использующие её. Вместо выполнения тестов сразу возвращается "пропущено" (skipped) или "отменено" с понятным сообщением, что сервис недоступен.

Состояния circuit breaker:

  • Замкнут (CLOSED) — сервис доступен, тесты выполняются.

  • Разомкнут (OPEN) — сервис недоступен, тесты пропускаются.

  • Полуоткрыт (HALF_OPEN) — после таймаута проверяется, восстановился ли сервис.

В тестировании обычно достаточно двух состояний: проверка перед запуском тестов, и если сервис не отвечает, все тесты, помеченные как зависимые, пропускаются.

Реализация

1. Python (pytest)

Используем фикстуру с проверкой и pytest.skip или маркер skipif.

import pytest
import requests

def is_service_available(url, timeout=2):
    try:
        response = requests.head(url, timeout=timeout)
        return response.status_code < 500
    except:
        return False

@pytest.fixture
def check_parts_service():
    if not is_service_available("http://localhost:8000/health"):
        pytest.skip("Parts service is not available")
    return True

# Использование
def test_search_parts(check_parts_service):
    # тест выполнится только если сервис доступен
    ...

# Или через условный маркер
service_available = is_service_available("http://localhost:8000/health")

@pytest.mark.skipif(not service_available, reason="Parts service is down")
def test_search_parts_2():
    ...

2. TypeScript (Jest / Playwright)

Jest: глобальная настройка или кастомный matcher

// setupTests.ts
import { checkService } from './service-check';

beforeAll(() => {
    if (!checkService('http://localhost:8000/health')) {
        console.warn('Service is down, skipping dependent tests');
    }
});

// Условный пропуск в тесте
test('external api test', async () => {
    if (!checkService('http://localhost:8000/health')) {
        test.skip(); // пропускаем тест
        return;
    }
    // сам тест
});

Вариативность реализации

  1. Глобальная проверка перед запуском всех тестов – если сервис недоступен, весь набор пропускается (экономит время).

  2. Индивидуальные проверки для каждого теста или группы – более гибко.

  3. Кэширование результата проверки – чтобы не дёргать сервис перед каждым тестом, а проверять раз в несколько минут.

  4. Проверка с экспоненциальным таймаутом – если сервис не отвечает, но мог бы восстановиться.

  5. Интеграция с логированием – запись факта пропуска тестов из-за недоступности сервиса.

  6. Комбинация с Retry – можно сделать несколько попыток проверки перед открытием цепи.

Когда применять, а когда нет

  • Применять:

    • В CI-среде, где тесты запускаются автоматически и могут зависеть от нестабильных внешних сервисов (например, тестовые стенды других команд).

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

    • Чтобы ускорить прогон тестов при падении внешней зависимости (не ждать таймаутов).

  • Не применять:

    • Если все зависимости критичны и должны быть доступны всегда (тогда лучше упасть с ошибкой).

    • Для тестов, проверяющих обработку ошибок при недоступности сервиса (наоборот, нужно проверить, что система корректно обрабатывает отказ).

    • В unit-тестах, где зависимости замоканы.

Связь с другими паттернами

  • Retry Pattern – circuit breaker можно комбинировать с retry: сначала несколько попыток доступа, затем размыкание цепи.

  • Health Check – часть реализации circuit breaker.

  • Dependency Injection – через DI можно подменять реальный сервис на мок, если он недоступен.

  • Waiter / Polling – может использоваться внутри проверки доступности.

Резюме

Circuit Breaker Pattern в автоматизации тестирования позволяет отключать тесты при недоступности внешних зависимостей, экономя ресурсы CI и предотвращая лавину ложных ошибок. Он особенно полезен в микросервисных архитектурах, где зависимости могут быть нестабильны. Реализация проста: проверка доступности перед тестом и пропуск с понятным сообщением. Не стоит злоупотреблять, чтобы не скрывать реальные проблемы с окружением.

Часть 4. Паттерны работы с внешними зависимостями (моки, стабы, контракты)

(изоляция от внешних API и сервисов)

17. API Mocking (перехват и подмена ответов)

Название и синонимы

API MockingMock ServerService VirtualizationHTTP InterceptionЗаглушки для API. В контексте UI-тестов также называют Network Interception или Request Mocking.

Проблема, которую решает

При тестировании приложений, особенно интеграционных и E2E-тестов, возникает ряд проблем, связанных с реальными API:

  • Нестабильность – внешние сервисы могут быть недоступны, медленны или возвращать ошибки.

  • Ограничения – платные API, лимиты на количество запросов, невозможность воспроизвести редкие сценарии (ошибки 500, таймауты).

  • Сложность тестирования граничных случаев – трудно заставить реальный сервис вернуть конкретную ошибку или задержку.

  • Параллельное выполнение – тесты могут влиять друг на друга через общий внешний сервис.

  • Скорость – реальные API обычно медленнее моков.

Суть паттерна

API Mocking заключается в подмене реальных HTTP-запросов к внешним сервисам на заранее подготовленные ответы. Это позволяет:

  • Изолировать тестируемый сервис от внешних зависимостей.

  • Контролировать ответы (статусы, тела, задержки) для проверки различных сценариев.

  • Ускорить выполнение тестов.

  • Обеспечить предсказуемость и повторяемость тестов.

Существует два основных подхода:

  1. На уровне клиента (UI-тесты) – перехват запросов в браузере и подмена ответов на лету (Playwright, Cypress, Puppeteer).

  2. На уровне сервера (API-тесты) – запуск отдельного мок-сервера, который эмулирует поведение реального API (WireMock, MockServer, самописные заглушки).

Реализация

1. UI-тесты: перехват запросов в браузере

Playwright (TypeScript) – перехват и подмена ответа

import { test, expect } from '@playwright/test';

test('mock API response in UI', async ({ page }) => {
  // Перехватываем запрос к API и подменяем ответ
  await page.route('**/api/inventory.json', async route => {
    // Вариант 1: полностью подменить ответ
    await route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify({
        items: [
          { id: 1, name: 'Mocked Item', price: 99.99 }
        ]
      })
    });
    
    // Вариант 2: изменить реальный ответ
    // const response = await route.fetch();
    // const json = await response.json();
    // json.items[0].price = 0; // модификация
    // await route.fulfill({ response, json });
  });

  await page.goto('https://example.com');
  // Теперь страница получит наши мок-данные
  await expect(page.locator('.item-price')).toHaveText('99.99');
});

Playwright (Python)

from playwright.sync_api import sync_playwright

def test_mock_api():
    with sync_playwright() as p:
        browser = p.chromium.launch()
        page = browser.new_page()
        
        def handle_route(route):
            route.fulfill(
                status=200,
                content_type="application/json",
                body='{"items": [{"id": 1, "name": "Mocked Item", "price": 99.99}]}'
            )
        
        page.route("**/api/inventory.json", handle_route)
        page.goto("https://example.com")
        # проверки...
        browser.close()

2. API-тесты: отдельный мок-сервер

WireMock (Java)

import com.github.tomakehurst.wiremock.WireMockServer;
import static com.github.tomakehurst.wiremock.client.WireMock.*;
import org.junit.jupiter.api.*;

class WireMockExample {
    private WireMockServer wireMockServer;

    @BeforeEach
    void setUp() {
        wireMockServer = new WireMockServer(8080);
        wireMockServer.start();
        configureFor("localhost", 8080);
    }

    @Test
    void testWithMock() {
        // Настраиваем мок
        stubFor(get(urlEqualTo("/api/parts/search?brand=BMW"))
            .willReturn(aResponse()
                .withStatus(200)
                .withHeader("Content-Type", "application/json")
                .withBody("{\"parts\": [{\"id\": 1, \"name\": \"Brake Pad\"}]}")));

        // Тестируем сервис, который обращается к моку
        Response response = given()
            .baseUri("http://localhost:8080")
            .get("/api/parts/search?brand=BMW");
        
        assertEquals(200, response.statusCode());
        assertEquals("Brake Pad", response.jsonPath().getString("parts[0].name"));
        
        // Проверяем, что запрос был сделан
        verify(getRequestedFor(urlEqualTo("/api/parts/search?brand=BMW")));
    }

    @AfterEach
    void tearDown() {
        wireMockServer.stop();
    }
}

3. Мок с динамическим управлением через админское API

Продвинутый подход – мок-сервер с собственным API для настройки поведения прямо из тестов (как в WireMock, но самописный).

Python (FastAPI) – мок с админским API

from fastapi import FastAPI
from pydantic import BaseModel
from typing import Dict, Any
import json

mock_app = FastAPI()
admin_app = FastAPI()

# Хранилище правил мокинга
rules: Dict[str, Any] = {}

class MockRule(BaseModel):
    path: str
    method: str
    status: int
    response: dict
    delay: float = 0.0

@admin_app.post("/__admin/mappings")
def add_rule(rule: MockRule):
    key = f"{rule.method}:{rule.path}"
    rules[key] = rule
    return {"status": "created"}

@mock_app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
async def catch_all(path: str, request: Request):
    key = f"{request.method}:{path}"
    if key in rules:
        rule = rules[key]
        if rule.delay > 0:
            await asyncio.sleep(rule.delay)
        return JSONResponse(status_code=rule.status, content=rule.response)
    return JSONResponse(status_code=404, content={"error": "No mock rule found"})

# Запуск двух приложений на разных портах или в одном с разными префиксами

Вариативность реализации

  1. Статические моки – заранее заданные ответы (простые JSON-файлы).

  2. Динамические моки – с возможностью менять поведение во время теста.

  3. Моки с задержками – для тестирования таймаутов и толерантности.

  4. Моки с проверкой вызовов (verify) – убедиться, что клиент действительно обратился к API.

  5. Запись-воспроизведение (record-replay) – сначала записываются реальные ответы, затем воспроизводятся в тестах.

  6. Моки для разных сценариев – позитивные, негативные, редкие ошибки.

  7. Иерархические моки – fallback на реальный API, если нет правила.

Когда применять, а когда нет

  • Применять:

    • При тестировании интеграции с внешними, нестабильными или платными API.

    • Для изоляции микросервисов при компонентном тестировании.

    • Для проверки обработки ошибок (500, 404, таймауты).

    • Для ускорения тестов.

  • Не применять:

    • В полноценных интеграционных тестах, где важно проверить реальное взаимодействие (тогда лучше использовать Testcontainers).

    • Если моки расходятся с реальным API и тесты теряют достоверность (нужно регулярно обновлять моки по контракту).

    • Для тестирования безопасности или производительности (там нужны реальные сервисы).

Связь с другими паттернами

  • Mock Service Pattern – частный случай API Mocking, когда создаётся отдельный сервис-заглушка.

  • Contract Testing – моки должны соответствовать контракту реального API.

  • Circuit Breaker – можно проверять, как клиент реагирует на недоступность мока.

  • Waiter Pattern – может использоваться внутри тестов с моками для проверки асинхронных вызовов.

Резюме

API Mocking – незаменимый паттерн для изоляции тестов от внешних зависимостей. Он даёт полный контроль над ответами, позволяет тестировать редкие сценарии и ускоряет выполнение тестов. В UI-тестах моки реализуются через перехват запросов в браузере, в API-тестах – через отдельные мок-серверы (WireMock, MockServer) или самописные заглушки. Важно помнить, что моки должны регулярно синхронизироваться с реальным API, чтобы тесты не потеряли достоверность.

18. Mock Service Pattern (самописная заглушка)

Название и синонимы

Mock Service PatternSelf-made Mock ServerTest Double as a ServiceDynamic MockЗаглушка с админским API. Этот паттерн развивает идею API Mocking, но делает акцент на создании отдельного сервиса, который полностью эмулирует поведение реального внешнего API и при этом предоставляет управляющий интерфейс (admin API) для динамической настройки ответов прямо из тестов.

Проблема, которую решает

Готовые инструменты для мокинга (WireMock, MockServer) хороши, но иногда требуют большего:

  • Гибкость – нужно программировать сложную логику ответов, зависящую от состояния, времени, комбинаций параметров.

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

  • Интеграция с тестами на том же языке – хочется управлять моком через тот же язык, на котором написаны тесты (Python/JS/Java).

  • Динамическое поведение – разные тесты требуют разных ответов, и менять конфигурацию мока нужно на лету, без перезапуска.

Mock Service Pattern предлагает создать самописный микросервис-заглушку, который:

  • Эмулирует внешний API (те же эндпоинты, форматы запросов/ответов).

  • Предоставляет админское API (обычно на отдельном порту или с префиксом /__admin) для управления поведением: установка ответов, задержек, ошибок, сброс истории.

  • Сохраняет историю запросов, чтобы тесты могли проверить факт обращения и параметры.

Суть паттерна

Mock Service Pattern – это создание отдельного сервиса, который живёт рядом с тестами (обычно поднимается в Docker-контейнере или как процесс перед тестами) и полностью замещает реальный внешний сервис. Ключевая особенность – наличие админского API, через которое тесты могут:

  • Настроить, какой ответ вернуть на конкретный запрос (статус, тело, заголовки).

  • Задать задержку перед ответом (для тестирования таймаутов).

  • Сбросить настройки к дефолтным.

  • Получить историю вызовов (сколько раз, с какими параметрами обращались).

Такой подход даёт полный контроль над внешней зависимостью и позволяет реализовать сложные сценарии тестирования.

Реализация

1. Python (FastAPI)

from fastapi import FastAPI, Request
from pydantic import BaseModel
from typing import Dict, Any, Optional, List
import json
import time
from datetime import datetime

app = FastAPI()
admin_app = FastAPI()  # отдельное приложение для админки, можно смонтировать на /__admin

# Хранилище правил и истории
rules: Dict[str, Dict[str, Any]] = {}
history: List[Dict[str, Any]] = []

class MappingRule(BaseModel):
    method: str
    path: str
    status: int = 200
    response: Any = None
    delay: float = 0.0
    headers: Dict[str, str] = {"Content-Type": "application/json"}

# Основное API (эмулирует внешний сервис)
@app.api_route("/api/v1/parts/search", methods=["GET"])
async def search_parts(brand: str = None, model: str = None):
    # Запоминаем вызов
    history.append({
        "timestamp": datetime.now().isoformat(),
        "method": "GET",
        "path": "/api/v1/parts/search",
        "params": {"brand": brand, "model": model}
    })
    
    # Ищем правило
    key = f"GET:/api/v1/parts/search"
    if key in rules:
        rule = rules[key]
        if rule["delay"] > 0:
            await asyncio.sleep(rule["delay"])
        
        # Можно учитывать параметры для более тонкой настройки
        return JSONResponse(
            status_code=rule["status"],
            content=rule["response"],
            headers=rule.get("headers", {})
        )
    
    # Дефолтный ответ
    return JSONResponse(
        status_code=200,
        content={"parts": [], "source": "mock"}
    )

# Админское API для управления моком
@admin_app.post("/__admin/mappings")
async def add_mapping(rule: MappingRule):
    key = f"{rule.method}:{rule.path}"
    rules[key] = rule.dict()
    return {"status": "created", "key": key}

@admin_app.get("/__admin/requests")
async def get_history():
    return history

@admin_app.delete("/__admin/reset")
async def reset():
    rules.clear()
    history.clear()
    return {"status": "reset"}

# Монтируем админку на префикс
app.mount("/__admin", admin_app)

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8081)

Использование в тестах (pytest)

import httpx
import pytest

@pytest.fixture
async def mock_client():
    async with httpx.AsyncClient(base_url="http://localhost:8081") as client:
        yield client

@pytest.fixture(autouse=True)
async def reset_mock(mock_client):
    """Сбрасываем мок перед каждым тестом"""
    await mock_client.delete("/__admin/reset")

async def test_search_parts_success(mock_client, api_client):
    # Настраиваем мок
    await mock_client.post("/__admin/mappings", json={
        "method": "GET",
        "path": "/api/v1/parts/search",
        "status": 200,
        "response": {
            "parts": [
                {"id": 1, "name": "Тормозные колодки", "price": 4500}
            ],
            "source": "mock"
        }
    })
    
    # Вызываем тестируемый сервис, который ходит в мок
    response = await api_client.search_parts("BMW", "X5")
    assert response.status_code == 200
    data = response.json()
    assert len(data["parts"]) == 1
    
    # Проверяем историю вызовов мока
    history = (await mock_client.get("/__admin/requests")).json()
    assert len(history) == 1
    assert history[0]["params"]["brand"] == "BMW"
    assert history[0]["params"]["model"] == "X5"

Использование в тестах (Playwright/TypeScript)

import { test, expect } from '@playwright/test';
import axios from 'axios';

const MOCK_URL = 'http://localhost:8081';

test.beforeEach(async () => {
    await axios.delete(`${MOCK_URL}/__admin/reset`);
});

test('should return mocked parts', async ({ request }) => {
    // Настраиваем мок
    await axios.post(`${MOCK_URL}/__admin/mappings`, {
        method: 'GET',
        path: '/api/v1/parts/search',
        status: 200,
        response: {
            parts: [{ id: 1, name: 'Brake Pads', price: 4500 }],
            source: 'mock'
        }
    });
    
    // Вызываем тестируемый сервис (который ходит в мок)
    const response = await request.get('http://localhost:8081/api/v1/parts/search', {
        params: { brand: 'BMW', model: 'X5' }
    });
    
    expect(response.ok()).toBeTruthy();
    const data = await response.json();
    expect(data.parts).toHaveLength(1);
    
    // Проверяем историю
    const history = (await axios.get(`${MOCK_URL}/__admin/requests`)).data;
    expect(history).toHaveLength(1);
    expect(history[0].params).toEqual({ brand: 'BMW', model: 'X5' });
});

Вариативность реализации

  1. Простая заглушка – фиксированные ответы для всех запросов.

  2. Правила по пути и методу – настройка ответов для конкретных эндпоинтов.

  3. Правила с условиями – по параметрам запроса, телу, заголовкам.

  4. Задержки (delays) – эмуляция медленных ответов.

  5. Сценарии (stateful) – изменение поведения в зависимости от количества вызовов.

  6. Запись и воспроизведение (record-replay) – сначала записываются реальные ответы, потом воспроизводятся.

  7. Проверка вызовов (history) – сбор информации о том, какие запросы приходили.

Когда применять, а когда нет

  • Применять:

    • Когда нужна максимальная гибкость и контроль над моком.

    • Если тесты пишутся на том же языке, что и мок (удобно поддерживать).

    • При тестировании сложных сценариев с зависящими от времени ответами.

    • Когда требуется проверять не только ответы, но и факт вызова с определёнными параметрами.

  • Не применять:

    • Если достаточно готового инструмента (WireMock, MockServer) – не изобретайте велосипед.

    • Если мок должен быть очень производительным (самописный может уступать).

    • В распределённых командах, где мок должен быть на другом стеке (тогда лучше использовать кроссплатформенные решения).

Связь с другими паттернами

  • API Mocking – частный случай, но с акцентом на самописную реализацию.

  • Contract Testing – мок должен соответствовать контракту реального API.

  • Circuit Breaker – можно проверять реакцию на недоступность мока.

  • Testcontainers – мок может запускаться в контейнере.

  • Dependency Injection – URL мока внедряется в тестируемый сервис.

Резюме

Mock Service Pattern – это создание выделенного сервиса-заглушки, полностью контролируемого через админское API. Он даёт максимальную гибкость в тестировании: от простых фиксированных ответов до сложных сценариев с задержками и проверкой вызовов. Особенно ценен, когда нужно тесно интегрировать мок с тестовым кодом на одном языке и когда готовые инструменты не обеспечивают нужного уровня контроля. Это senior-уровень подхода к мокированию, демонстрирующий глубокое понимание архитектуры тестирования.

19. Schema Validation (валидация контракта ответа)

Название и синонимы

Schema ValidationContract ValidationJSON Schema ValidationResponse Structure ValidationСхемная валидация. В разных экосистемах используются библиотеки: AjvZod (JS), Pydantic (Python), json-schema-validator (Java), Joi (Node.js).

Проблема, которую решает

При тестировании API мы часто проверяем отдельные поля ответа: статус код, наличие определённых ключей, конкретные значения. Но этого недостаточно:

  • Незаметные изменения – разработчик бэкенда может случайно изменить тип поля (число стало строкой), переименовать ключ или удалить обязательное поле. Тест, проверяющий только наличие поля name, не заметит, что его тип изменился с string на object.

  • Дрейф контракта – со временем API может незаметно отклоняться от документации, что приведёт к проблемам у клиентов.

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

  • Отсутствие живой документации – тесты не отражают полную структуру ответа.

Суть паттерна

Schema Validation заключается в описании ожидаемой структуры ответа API в виде схемы (JSON Schema, XSD, или декларативного описания в коде) и проверке каждого ответа на соответствие этой схеме. Схема определяет:

  • Какие поля обязательны, какие опциональны.

  • Типы полей (string, number, array, object).

  • Форматы (email, date, uuid).

  • Допустимые значения (enum, pattern).

  • Вложенность и структуру массивов.

Вместо десятка ассертов мы пишем одну проверку на схему, которая гарантирует, что ответ соответствует контракту. При изменении API мы обновляем схему в одном месте.

1. Python (библиотека jsonschema)

from jsonschema import validate, ValidationError
import pytest

# Определяем схему
search_response_schema = {
    "type": "object",
    "required": ["source", "parts", "total"],
    "properties": {
        "source": {
            "type": "string",
            "enum": ["cache", "external", "database"]
        },
        "total": {
            "type": "integer",
            "minimum": 0
        },
        "parts": {
            "type": "array",
            "items": {
                "type": "object",
                "required": ["id", "name", "price", "oem_number"],
                "properties": {
                    "id": {"type": "integer"},
                    "name": {"type": "string", "minLength": 1},
                    "price": {"type": "number", "minimum": 0},
                    "oem_number": {"type": "string", "pattern": "^[A-Z0-9-]+$"},
                    "description": {"type": "string", "maxLength": 500}
                }
            }
        },
        "error": {"type": "string"}  # опционально, появляется при ошибке
    },
    "additionalProperties": False  # запрещаем лишние поля
}

def validate_search_response(data):
    try:
        validate(instance=data, schema=search_response_schema)
        return True, []
    except ValidationError as e:
        return False, [e.message]

# Использование в тесте
def test_search_parts_schema(api_client):
    response = api_client.search_parts("BMW", "X5")
    assert response.status_code == 200
    
    is_valid, errors = validate_search_response(response.json())
    assert is_valid, f"Schema validation failed: {errors}"

Использование Pydantic (более современный подход)

from pydantic import BaseModel, Field, validator
from typing import List, Optional
from enum import Enum

class SourceEnum(str, Enum):
    cache = "cache"
    external = "external"
    database = "database"

class Part(BaseModel):
    id: int
    name: str = Field(..., min_length=1)
    price: float = Field(..., ge=0)
    oem_number: str = Field(..., regex="^[A-Z0-9-]+$")
    description: Optional[str] = Field(None, max_length=500)

class SearchResponse(BaseModel):
    source: SourceEnum
    total: int = Field(..., ge=0)
    parts: List[Part]
    error: Optional[str] = None

    @validator('total')
    def total_matches_parts_count(cls, v, values):
        if 'parts' in values and v != len(values['parts']):
            raise ValueError('total must equal number of parts')
        return v

# Использование
def test_search_parts_pydantic(api_client):
    response = api_client.search_parts("BMW", "X5")
    assert response.status_code == 200
    
    # Pydantic автоматически валидирует и преобразует данные
    search_response = SearchResponse(**response.json())
    # Если данные не соответствуют схеме, будет исключение
    assert search_response.source in ["cache", "external"]

2. TypeScript/JavaScript (библиотеки Zod)

import { z } from 'zod';

const PartSchema = z.object({
    id: z.number().int().positive(),
    name: z.string().min(1),
    price: z.number().nonnegative(),
    oem_number: z.string().regex(/^[A-Z0-9-]+$/),
    description: z.string().max(500).optional()
});

const SearchResponseSchema = z.object({
    source: z.enum(['cache', 'external', 'database']),
    total: z.number().int().nonnegative(),
    parts: z.array(PartSchema),
    error: z.string().optional()
}).refine(data => data.total === data.parts.length, {
    message: "total must equal number of parts"
});

test('API response validation with Zod', async ({ request }) => {
    const response = await request.get('/api/v1/parts/search', {
        params: { brand: 'BMW', model: 'X5' }
    });
    expect(response.ok()).toBeTruthy();
    
    const data = await response.json();
    const result = SearchResponseSchema.safeParse(data);
    
    if (!result.success) {
        console.log('Validation errors:', result.error.format());
    }
    expect(result.success).toBe(true);
});

3. Java (библиотеки json-schema-validator, Jackson)

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.networknt.schema.JsonSchema;
import com.networknt.schema.JsonSchemaFactory;
import com.networknt.schema.SpecVersion;
import com.networknt.schema.ValidationMessage;
import org.junit.jupiter.api.Test;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.*;

class SchemaValidationTest {
    
    private final ObjectMapper mapper = new ObjectMapper();
    private final JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7);
    
    @Test
    void testSearchPartsSchema() throws Exception {
        // Загружаем схему из файла или строки
        String schemaString = """
            {
                "type": "object",
                "required": ["source", "parts", "total"],
                "properties": {
                    "source": {"type": "string", "enum": ["cache", "external"]},
                    "total": {"type": "integer", "minimum": 0},
                    "parts": {
                        "type": "array",
                        "items": {
                            "type": "object",
                            "required": ["id", "name"],
                            "properties": {
                                "id": {"type": "integer"},
                                "name": {"type": "string"}
                            }
                        }
                    }
                }
            }
            """;
        
        JsonSchema schema = factory.getSchema(schemaString);
        
        // Получаем ответ API (пример)
        String responseJson = """
            {
                "source": "cache",
                "total": 2,
                "parts": [
                    {"id": 1, "name": "Brake Pad"},
                    {"id": 2, "name": "Oil Filter"}
                ]
            }
            """;
        
        JsonNode responseNode = mapper.readTree(responseJson);
        
        // Валидируем
        Set<ValidationMessage> errors = schema.validate(responseNode);
        
        assertTrue(errors.isEmpty(), "Schema validation errors: " + errors);
    }
}

Вариативность реализации

  1. JSON Schema (стандарт) – независимый от языка способ описания контракта. Можно хранить схемы в отдельных файлах и переиспользовать между проектами.

  2. Библиотеки с декларативным синтаксисом (Zod, Pydantic) – схемы описываются в коде, что даёт автодополнение и интеграцию с типами.

  3. Комбинированный подход – сначала валидация схемы, затем точечные проверки бизнес-логики.

  4. Генерация схем из кода – Pydantic, Zod могут генерировать JSON Schema автоматически.

  5. Валидация частей ответа – можно валидировать не весь ответ, а только вложенные структуры.

  6. Строгость – разрешать или запрещать дополнительные поля (additionalProperties).

Когда применять, а когда нет

  • Применять:

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

    • В контрактном тестировании (Consumer-Driven Contracts).

    • Для раннего обнаружения breaking changes при обновлении зависимостей.

    • Когда API имеет сложную структуру с множеством полей.

    • В регрессионном тестировании – убедиться, что изменения не сломали контракт.

  • Не применять:

    • Для очень простых ответов (например, {"status": "ok"}) – избыточно.

    • Если API часто меняется и схемы не успевают обновляться (но это симптом проблемы).

    • Когда важна производительность (валидация сложных схем может быть затратной, но для тестов это обычно не критично).

Связь с другими паттернами

  • Contract Testing – Schema Validation является основой для контрактного тестирования.

  • Snapshot Testing – схема проверяет структуру, снэпшот – конкретные данные.

  • API Client Wrapper – можно добавить автоматическую валидацию ответов в клиенте.

  • Data-Driven Testing – схемы могут использоваться для проверки всех ответов в параметризованных тестах.

Резюме

Schema Validation – это мощный паттерн, который поднимает тестирование API на новый уровень. Вместо разрозненных проверок отдельных полей мы описываем полный контракт API и автоматически проверяем соответствие ему каждого ответа. Это позволяет:

  • Обнаруживать breaking changes на ранних стадиях (до выхода в прод).

  • Сокращать количество кода в тестах.

  • Иметь живую документацию в виде схем.

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

Паттерн особенно ценен в микросервисной архитектуре, где множество сервисов общаются через API, и соблюдение контрактов критически важно.

20. Contract Testing (Pact) / Consumer-Driven Contracts

Название и синонимы

Contract TestingConsumer-Driven Contract Testing (CDC) , Pact TestingКонтрактное тестирование. Основной инструмент — Pact (существуют реализации для всех популярных языков). Также существуют Spring Cloud Contract, но Pact — наиболее распространённый и кросс-языковой стандарт.

Проблема, которую решает

В микросервисной архитектуре сервисы постоянно общаются через API. Интеграционные тесты, проверяющие взаимодействие всех сервисов вместе, медленные, хрупкие и сложны в поддержке. Мокирование на стороне потребителя не гарантирует, что реальный провайдер действительно вернёт то, что ожидает потребитель. В результате:

  • Изменения в провайдере могут незаметно сломать потребителя.

  • Команды синхронизируются вручную, тратя время на согласования.

  • Баги интеграции обнаруживаются поздно — на staging или в проде.

Суть паттерна

Consumer-Driven Contract Testing — это подход, при котором потребитель (consumer) API определяет свои ожидания от провайдера (provider) в виде контракта. Провайдер затем проверяет, что он соответствует всем контрактам своих потребителей .

Основные принципы:

  1. Consumer-driven — именно потребитель диктует, что ему нужно от API.

  2. Контракт — файл (обычно JSON), описывающий конкретные взаимодействия: запрос и ожидаемый ответ.

  3. Независимое тестирование — потребитель тестируется с мок-сервером, провайдер — с верификатором контрактов.

  4. Pact Broker — центральное хранилище контрактов и результатов верификации .

Реализация

1. Python (Pact)

my_project/
├── consumer/
│   ├── client.py          # Клиент, который вызывает API
│   └── tests/
│       └── test_contract.py
├── provider/
│   ├── app.py              # FastAPI приложение
│   └── tests/
│       └── test_verify_contract.py
└── pacts/                  # Сгенерированные контракты

Consumer (клиент и тест)

# consumer/client.py
import httpx
from dataclasses import dataclass

@dataclass
class User:
    id: int
    name: str
    created_on: str

class UserClient:
    def __init__(self, base_url: str):
        self.base_url = base_url

    def get_user(self, user_id: int) -> User:
        response = httpx.get(f"{self.base_url}/users/{user_id}")
        response.raise_for_status()
        data = response.json()
        return User(
            id=data["id"],
            name=data["name"],
            created_on=data["created_on"]
        )

Consumer тест (pytest)

# consumer/tests/test_contract.py
import pytest
from pact import Pact, like, term
from consumer.client import UserClient

@pytest.fixture
def pact():
    """Создаём Pact для потребителя 'user-consumer' и провайдера 'user-provider'."""
    pact = Pact("user-consumer", "user-provider")
    yield pact
    pact.write_file("./pacts")  # Сохраняем контракт после теста

def test_get_user(pact):
    # Определяем ожидаемый ответ с матчерами (гибкая валидация)
    expected_response = {
        "id": like(123),                # любое целое число
        "name": term("Alice", "\\w+"),   # строка, соответствующая regexp
        "created_on": term("2024-01-01T00:00:00", "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}")
    }

    # Описываем взаимодействие
    (pact
     .given("user exists", id=123, name="Alice")  # состояние провайдера
     .upon_receiving("a request for user 123")
     .with_request("GET", "/users/123")
     .will_respond_with(200, body=expected_response))

    # Запускаем мок-сервер Pact
    with pact.serve() as server:
        client = UserClient(server.url)
        user = client.get_user(123)

        assert user.id == 123
        assert user.name == "Alice"

Что здесь важно:

  • given описывает состояние провайдера (например, "пользователь существует") и может передавать параметры .

  • liketerm — это матчеры, делающие контракт гибким: важен тип и формат, а не точное значение .

  • pact.serve() запускает мок-сервер, который ведёт себя строго по описанному взаимодействию.

Provider (верификация)

# provider/app.py (FastAPI)
from fastapi import FastAPI
from datetime import datetime

app = FastAPI()

users_db = {
    123: {"id": 123, "name": "Alice", "created_on": "2024-01-01T10:00:00"}
}

@app.get("/users/{user_id}")
def get_user(user_id: int):
    return users_db.get(user_id, {"error": "not found"})

Provider тест (верификация контракта)

# provider/tests/test_verify_contract.py
import pytest
from pact import Verifier

def test_verify_user_contract():
    # Настраиваем обработчики состояний провайдера
    def user_exists(action, parameters):
        if action == "setup" and parameters.get("id") == 123:
            # Создаём тестового пользователя (в БД или in-memory)
            users_db[123] = {
                "id": 123,
                "name": parameters.get("name", "Alice"),
                "created_on": "2024-01-01T10:00:00"
            }
        elif action == "teardown" and parameters.get("id") == 123:
            # Очищаем после теста
            users_db.pop(123, None)

    verifier = (
        Verifier("user-provider")
        .add_transport(url="http://localhost:8000")  # где запущен провайдер
        .add_source("../pacts/user-consumer-user-provider.json")
        .state_handler({"user exists": user_exists}, teardown=True)
    )

    # Запускаем верификацию
    verifier.verify()

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

2. Java (JUnit5 + Pact)

Consumer тест (JUnit5)

import au.com.dius.pact.consumer.dsl.PactDslWithProvider;
import au.com.dius.pact.consumer.junit5.PactConsumerTestExt;
import au.com.dius.pact.consumer.junit5.PactTestFor;
import au.com.dius.pact.core.model.V4Pact;
import au.com.dius.pact.core.model.annotations.Pact;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.web.client.RestTemplate;

import java.util.Map;

@ExtendWith(PactConsumerTestExt.class)
public class UserConsumerPactTest {

    @Pact(consumer = "user-consumer", provider = "user-provider")
    public V4Pact createPact(PactDslWithProvider builder) {
        return builder
            .given("user exists", Map.of("id", 123, "name", "Alice"))
            .uponReceiving("a request for user 123")
            .path("/users/123")
            .method("GET")
            .willRespondWith()
            .status(200)
            .body("{\"id\": 123, \"name\": \"Alice\", \"created_on\": \"2024-01-01T10:00:00\"}")
            .toPact(V4Pact.class);
    }

    @Test
    @PactTestFor(providerName = "user-provider", port = "8080")
    void testGetUser() {
        RestTemplate restTemplate = new RestTemplate();
        String response = restTemplate.getForObject("http://localhost:8080/users/123", String.class);
        // проверки
    }
}

Provider тест (JUnit5)

import au.com.dius.pact.provider.junit5.HttpTestTarget;
import au.com.dius.pact.provider.junit5.PactVerificationContext;
import au.com.dius.pact.provider.junit5.PactVerificationInvocationContextProvider;
import au.com.dius.pact.provider.junitsupport.Provider;
import au.com.dius.pact.provider.junitsupport.State;
import au.com.dius.pact.provider.junitsupport.loader.PactBroker;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.TestTemplate;
import org.junit.jupiter.api.extension.ExtendWith;

@Provider("user-provider")
@PactBroker(url = "http://localhost:9292") // или локальная папка
public class UserProviderPactTest {

    @TestTemplate
    @ExtendWith(PactVerificationInvocationContextProvider.class)
    void verifyPact(PactVerificationContext context) {
        context.verifyInteraction();
    }

    @BeforeEach
    void setTarget(PactVerificationContext context) {
        context.setTarget(new HttpTestTarget("localhost", 8080));
    }

    @State("user exists")
    public void userExists(Map<String, Object> params) {
        Long id = (Long) params.get("id");
        String name = (String) params.get("name");
        // настройка тестовых данных
        setupTestUser(id, name);
    }
}

Рабочий процесс с Pact Broker

  1. Consumer запускает тесты, генерирует контракты и публикует их в Pact Broker.

  2. Provider забирает контракты из Broker, верифицирует их и публикует результаты верификации.

  3. Pact Broker хранит историю контрактов и результатов, позволяя использовать "can-i-deploy" для безопасного развёртывания .

Вариативность реализации

  1. HTTP-взаимодействия — классический случай (REST, GraphQL).

  2. Message Pacts — для асинхронного взаимодействия (Kafka, RabbitMQ) .

  3. Pact Broker с версионированием — можно выбирать контракты по тегам (main, prod) и веткам.

  4. Матчеры — вместо точных значений используем liketermeachLike для гибкости .

  5. Параметризованные состояния — передача параметров из consumer в provider state handler .

Когда применять, а когда нет

  • Применять:

    • Для всех критических интеграций между микросервисами.

    • Когда над разными сервисами работают разные команды.

    • В CI/CD для обеспечения обратной совместимости перед деплоем.

  • Не применять:

    • Для публичных API, где потребители неизвестны (тогда лучше OpenAPI + схема).

    • В монолитах, где всё тестируется интеграционными тестами.

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

Связь с другими паттернами

  • API Client Wrapper — клиент, который тестируется через Pact, должен использовать внедрение URL, чтобы его можно было перенаправить на мок-сервер.

  • Schema Validation — матчеры Pact выполняют ту же функцию, что и схемы.

  • Mock Service Pattern — Pact на стороне consumer использует мок-сервер.

  • Dependency Injection — URL провайдера внедряется в клиент.

Резюме

Contract Testing с Pact — это промышленный стандарт для тестирования интеграций в микросервисной архитектуре. Он позволяет:

  • Обнаруживать breaking changes до деплоя.

  • Давать командам независимость — consumer диктует, provider подстраивается.

  • Иметь централизованное хранилище контрактов (Pact Broker) с историей и матрицей совместимости.

  • Заменить медленные и хрупкие end-to-end тесты быстрыми, изолированными и надёжными контрактными тестами.

Это senior-level паттерн, демонстрирующий глубокое понимание архитектуры микросервисов и современных практик обеспечения надёжности интеграций.

Часть 5. Паттерны оптимизации и параллельного выполнения

(ускорение тестов и эффективное использование ресурсов)

21. Parallel Execution (параллельный запуск тестов)

Название и синонимы

Parallel ExecutionParallel TestingConcurrent TestingМногопоточный запуск тестов. В разных фреймворках: workers (Playwright), parallel (JUnit5), -n (pytest-xdist), threadCount (TestNG).

Проблема, которую решает

По мере роста количества тестов время их выполнения становится критическим фактором. Прогон всех тестов последовательно (в один поток) может занимать часы, что:

  • Замедляет обратную связь для разработчиков.

  • Увеличивает время выполнения CI/CD пайплайнов.

  • Снижает эффективность использования ресурсов (многоядерные процессоры простаивают).

  • Затрудняет быстрое итерирование и частые релизы.

Суть паттерна

Parallel Execution заключается в одновременном запуске нескольких тестов в разных потоках, процессах или на разных машинах. Тесты распределяются между доступными исполнител��ми (воркерами), что позволяет сократить общее время прогона пропорционально количеству воркеров (с учётом накладных расходов).

Ключевые требования для успешного параллельного запуска:

  • Изоляция тестов – тесты не должны влиять друг на друга (общие данные, состояние).

  • Безопасность ресурсов – файлы, БД, порты должны быть разделены или корректно синхронизированы.

  • Независимость – порядок выполнения не должен влиять на результат.

Реализация

1. Python (pytest)

Установка плагина pytest-xdist

Настройка в pytest.ini

[pytest]
addopts = -n auto

Проблемы и их решение

При параллельном запуске тесты могут конфликтовать из-за общего состояния БД. Решение – изоляция через транзакции или уникальные данные:

# conftest.py
import pytest
from faker import Faker

fake = Faker()

@pytest.fixture
def unique_user():
    """Каждый тест получает уникального пользователя."""
    return {
        "username": fake.user_name(),
        "email": fake.email(),
        "password": fake.password()
    }

@pytest.fixture(autouse=True)
def clean_database():
    """Очистка БД перед каждым тестом (если нужно)."""
    db.execute("TRUNCATE users RESTART IDENTITY CASCADE")
    yield

2. TypeScript (Playwright)

Настройка в playwright.config.ts

import { defineConfig } from '@playwright/test';

export default defineConfig({
  // Количество параллельных воркеров
  workers: 4,  // или '50%' для использования половины ядер
  
  // Полностью параллельно внутри каждого файла
  fullyParallel: true,
  
  projects: [
    {
      name: 'chromium',
      use: { ... },
      workers: 2,  // переопределение для проекта
    },
    {
      name: 'firefox',
      use: { ... },
      workers: 2,
    }
  ]
});

Шардирование (sharding) для CI Playwright поддерживает шардирование – разделение тестов между несколькими машинами:

# На первой машине
npx playwright test --shard=1/3

# На второй машине
npx playwright test --shard=2/3

# На третьей машине
npx playwright test --shard=3/3

Изоляция в Playwright Playwright автоматически создаёт изолированный контекст для каждого теста (если не используются глобальные фикстуры). Для работы с БД:

// fixtures.ts
import { test as base } from '@playwright/test';
import { prisma } from './prisma';

export const test = base.extend({
  // Каждый тест получает чистую транзакцию
  db: async ({}, use) => {
    await prisma.$transaction(async (tx) => {
      await use(tx);
      throw new Error('Rollback');  // откат после теста
    });
  },
});

3. Java (JUnit5)

Настройка параллельного выполнения

В JUnit5 параллелизм настраивается через конфигурационный файл junit-platform.properties:

# junit-platform.properties
junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = concurrent
junit.jupiter.execution.parallel.config.strategy = dynamic
junit.jupiter.execution.parallel.config.dynamic.factor = 0.5  # половина ядер

Аннотации для управления параллелизмом

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.parallel.Execution;
import org.junit.jupiter.api.parallel.ExecutionMode;

class ParallelTest {
    
    @Test
    @Execution(ExecutionMode.CONCURRENT)
    void test1() {
        // будет выполняться параллельно
    }
    
    @Test
    @Execution(ExecutionMode.SAME_THREAD)
    void test2() {
        // будет выполняться в том же потоке, что и вызывающий
    }
}

Синхронизация ресурсовЕсли тесты используют общие ресурсы, можно использовать аннотацию @ResourceLock:

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.parallel.ResourceLock;

class SharedResourceTest {
    
    @Test
    @ResourceLock("database")
    void testDatabase1() {
        // доступ к БД
    }
    
    @Test
    @ResourceLock("database")
    void testDatabase2() {
        // эти тесты не будут выполняться параллельно с testDatabase1
    }
}

Вариативность реализации

  1. Параллелизм на уровне методов – несколько тестов в одном классе выполняются одновременно.

  2. Параллелизм на уровне классов – разные тестовые классы выполняются параллельно.

  3. Параллелизм на уровне модулей – целые модули тестов запускаются параллельно.

  4. Шардирование (sharding) – разделение тестов между несколькими машинами/контейнерами в CI.

  5. Параллелизм с динамическим числом воркеров – автоматический выбор количества потоков под нагрузку.

  6. Параллелизм по браузерам/устройствам – в Playwright отдельные проекты для разных браузеров могут выполняться параллельно.

Когда применять, а когда нет

  • Применять:

    • Всегда в CI, когда тестов много и время критично.

    • На локальных машинах с многоядерными процессорами.

    • Для ускорения обратной связи при разработке.

  • Не применять:

    • Если тесты не изолированы и влияют друг на друга (сначала нужно исправить).

    • Для тестов, работающих с эксклюзивными ресурсами (например, захват порта).

    • Если накладные расходы на параллелизацию превышают выгоду (очень мало тестов).

Важные требования для параллельного запуска

  1. Изоляция данных – каждый тест должен работать со своими данными или очищать за собой.

    python

    @pytest.fixture
    def unique_email():
        return f"user_{uuid.uuid4()}@example.com"
  2. Изоляция файловой системы – тесты должны писать во временные уникальные директории.

    java

    @TempDir
    Path tempDir;  // JUnit5 создаёт уникальную временную папку
  3. Изоляция портов – если тесты поднимают серверы, использовать случайные свободные порты.

    typescript

    const port = await getRandomFreePort();
  4. Безопасность общих ресурсов – использовать синхронизацию для доступа к общим данным.

    java

    @ResourceLock("database")

Связь с другими паттернами

  • Transaction Tests / Clean State – обязателен для параллельного запуска тестов с БД.

  • Test Data Factory с Faker – генерация уникальных данных для избежания конфликтов.

  • Dependency Injection – фикстуры должны создавать новые экземпляры для каждого теста (scope function).

  • Retry Pattern – может быть полезен при параллельных гонках, но лучше устранить причину.

Резюме

Parallel Execution – критически важный паттерн для современных CI/CD пайплайнов, позволяющий сократить время выполнения тестов с часов до минут. Он требует тщательной изоляции тестов и правильной архитектуры, но окупается многократно. Современные фреймворки (Playwright, pytest, JUnit) предоставляют встроенную поддержку параллельного запуска, и его использование – обязательный элемент зрелого тестового фреймворка.

22. Lazy Setup / Caching of Heavy Resources

Название и синонимы

Lazy SetupCaching of Heavy ResourcesSession-scoped FixturesSingleton ResourcesResource PoolingЛенивая инициализация тяжёлых ресурсов. В контексте тестовых фреймворков часто реализуется через фикстуры с областью видимости session (pytest), singleton (Playwright), или через паттерн Singleton в коде.

Проблема, которую решает

В автоматизированных тестах часто используются "тяжёлые" ресурсы:

  • HTTP-клиенты с пулом соединений.

  • Подключения к базе данных.

  • Драйверы браузера (WebDriver, Playwright).

  • Загрузка конфигурационных файлов.

  • Получение OAuth-токенов (требующих обращения к Auth-серверу).

  • Инициализация DI-контейнеров.

Если создавать эти ресурсы заново для каждого теста, возникают проблемы:

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

  • Избыточная нагрузка – многократное создание подключений может исчерпать лимиты (например, максимальное число соединений к БД).

  • Долгие прогоны – тесты, которые могли бы выполняться секунды, растягиваются на минуты из-за повторяющейся инициализации.

Суть паттерна

Lazy Setup / Caching заключается в том, что тяжёлые ресурсы создаются один раз (обычно при первом обращении) и затем переиспользуются во всех тестах, где они нужны. Ресурс живёт на протяжении всей тестовой сессии и корректно освобождается после завершения всех тестов.

Ключевые аспекты:

  • Ленивость – ресурс создаётся только когда действительно требуется, а не до начала тестов.

  • Кеширование – один экземпляр используется многократно.

  • Потокобезопасность – при параллельном выполнении доступ к ресурсу должен быть синхронизирован (или ресурс должен быть потоко-безопасным).

  • Освобождение – ресурс должен корректно закрываться после всех тестов.

Реализация

1. Python (pytest) – session-scoped фикстуры

2. TypeScript (Playwright) – worker-scoped фикстуры

В Playwright фикстуры могут иметь область видимости worker (один экземпляр на воркер, т.е. на процесс) или test.

import { test as base, request } from '@playwright/test';

// Типы для наших фикстур
type MyFixtures = {
  apiClient: ApiClient;
  authToken: string;
  dbConnection: DbConnection;
};

// Расширяем базовый объект test
export const test = base.extend<MyFixtures>({
  // API клиент на весь воркер
  apiClient: [async ({}, use) => {
    const client = await ApiClient.create('https://api.example.com');
    await use(client);
    await client.dispose();
  }, { scope: 'worker' }],

  // Токен, получаемый один раз на воркер
  authToken: [async ({ apiClient }, use) => {
    const token = await apiClient.authenticate('test', 'password');
    await use(token);
    // токен не требует явной очистки
  }, { scope: 'worker' }],

  // Подключение к БД (пример)
  dbConnection: [async ({}, use) => {
    const connection = await DbConnection.connect(process.env.DATABASE_URL);
    await use(connection);
    await connection.close();
  }, { scope: 'worker' }]
});

export { expect } from '@playwright/test';

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

import { test } from './fixtures';

test('get user profile', async ({ apiClient, authToken }) => {
  const response = await apiClient.get('/users/me', {
    headers: { Authorization: `Bearer ${authToken}` }
  });
  expect(response.status).toBe(200);
});

test('another test using same client', async ({ apiClient }) => {
  // Используется тот же экземпляр apiClient
});

Ленивая инициализация через геттеры

class TestContext {
  private static _dbConnection: DbConnection | null = null;
  
  static async getDbConnection(): Promise<DbConnection> {
    if (!this._dbConnection) {
      this._dbConnection = await DbConnection.connect(process.env.DATABASE_URL);
      // Регистрируем очистку после всех тестов
      process.on('exit', () => this._dbConnection?.close());
    }
    return this._dbConnection;
  }
}

3. Java (JUnit5) – синглтоны и @BeforeAll

В Java можно использовать статические поля и @BeforeAll для инициализации один раз на класс, или более сложные механизмы для сессионных ресурсов.

Использование Testcontainers с сессионным контейнером

import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

@Testcontainers
class UserRepositoryTest {
    
    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:13")
            .withDatabaseName("testdb");
    
    private static Connection connection;
    
    @BeforeAll
    static void initConnection() throws SQLException {
        connection = DriverManager.getConnection(
            postgres.getJdbcUrl(),
            postgres.getUsername(),
            postgres.getPassword()
        );
    }
    
    @AfterAll
    static void closeConnection() throws SQLException {
        if (connection != null) {
            connection.close();
        }
    }
    
    @Test
    void testDatabase() throws SQLException {
        // используем connection
    }
}

Вариативность реализации

  1. Фикстуры с областью видимости (session/worker) – самый удобный способ в pytest и Playwright.

  2. Синглтоны – классический паттерн для глобального доступа к ресурсу.

  3. Кеширование через статические поля – в Java/JUnit.

  4. Пул ресурсов – для случаев, когда один ресурс не может быть использован параллельно, создаётся пул (например, пул браузеров).

  5. Ленивая инициализация с мемоизацией – @Memoized в некоторых библиотеках.

  6. Контекстное кеширование – в Spring Test контекст приложения кешируется между тестами.

Когда применять, а когда нет

  • Применять:

    • Для всех "тяжёлых" ресурсов: HTTP-клиенты, подключения к БД, драйверы браузера.

    • Для ресурсов с длительной инициализацией: загрузка конфигурации, получение токенов.

    • Для ресурсов с ограничениями на количество (лимиты соединений).

  • Не применять:

    • Для легковесных объектов (стоимость создания мала).

    • Если ресурс хранит состояние между тестами, которое может влиять на результаты (тогда нужно изолировать).

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

Связь с другими паттернами

  • Dependency Injection – фикстуры с scope="session" – это форма DI с управлением жизненным циклом.

  • Singleton – часто используется для реализации кеширования ресурсов.

  • Pooling Pattern – расширение для параллельного использования ограниченных ресурсов.

  • Lazy Initialization – базовая техника для отложенного создания.

Пример кода (расширенный с пулом ресурсов)

Python: пул браузеров для параллельных тестов

import pytest
from playwright.sync_api import sync_playwright

class BrowserPool:
    def __init__(self, size=2):
        self.playwright = sync_playwright().start()
        self.browsers = [self.playwright.chromium.launch() for _ in range(size)]
        self.available = list(range(size))
        self.lock = threading.Lock()
    
    def get_browser(self):
        with self.lock:
            if not self.available:
                raise RuntimeError("No available browsers")
            idx = self.available.pop()
            return self.browsers[idx], idx
    
    def return_browser(self, idx):
        with self.lock:
            self.available.append(idx)
    
    def close(self):
        for browser in self.browsers:
            browser.close()
        self.playwright.stop()

@pytest.fixture(scope="session")
def browser_pool():
    pool = BrowserPool()
    yield pool
    pool.close()

@pytest.fixture
def browser(browser_pool):
    browser, idx = browser_pool.get_browser()
    yield browser
    browser_pool.return_browser(idx)

Резюме

Lazy Setup / Caching of Heavy Resources – критически важный паттерн для оптимизации производительности тестов. Он позволяет сократить время выполнения тестов в разы за счёт однократного создания тяжёлых объектов и их переиспользования. В современных тестовых фреймворках (pytest, Playwright, JUnit) есть встроенные механизмы (scope="session", worker-scoped fixtures, статические инициализаторы), которые делают реализацию этого паттерна простой и безопасной. Правильное применение кеширования ресурсов – признак зрелого и производительного тестового фреймворка.

23. Proxy Pattern для логирования и метрик

Название и синонимы

Proxy PatternWrapper PatternInterceptorДекоратор (близкий паттерн, но с акцентом на добавление функциональности). В контексте тестирования часто называют Logging ProxyPerformance ProxyMonitoring Wrapper.

Проблема, которую решает

В автоматизированных тестах часто требуется добавлять сквозную функциональность (cross-cutting concerns) к уже существующим объектам:

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

  • Замер времени выполнения для выявления медленных операций.

  • Сбор метрик (количество вызовов, частота ошибок).

  • Добавление повторных попыток (retry) к нестабильным методам.

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

Если добавлять эту функциональность в каждый класс (Page Object, API Client) вручную, код быстро превращается в "спагетти" с повторяющейся логикой. Нарушается принцип единственной ответственности (SRP) – классы занимаются не только своей прямой работой, но и логированием, метриками и т.д.

Суть паттерна

Proxy Pattern предлагает создать объект-обёртку, который перехватывает вызовы к исходному объекту и добавляет нужную функциональность до и/или после вызова. Прокси имеет тот же интерфейс, что и оригинальный объект, поэтому клиент (тест) не замечает подмены.

В тестировании прокси особенно полезны для:

  • Логирования запросов/ответов API без изменения кода клиента.

  • Измерения времени выполнения методов Page Object.

  • Автоматического скриншота при ошибке в UI-тестах.

  • Добавления retry к нестабильным методам.

Реализация

1. Python (декораторы и прокси-классы)

Прокси для HTTP-клиента (API)

import time
import logging
from functools import wraps

logger = logging.getLogger(__name__)

class ApiClientProxy:
    """Прокси для HTTP-клиента, добавляющий логирование и замер времени."""
    
    def __init__(self, client):
        self._client = client
    
    async def get(self, url, **kwargs):
        start = time.time()
        try:
            response = await self._client.get(url, **kwargs)
            duration = time.time() - start
            logger.info(f"GET {url} - {response.status_code} - {duration:.3f}s")
            return response
        except Exception as e:
            logger.error(f"GET {url} failed: {e}")
            raise
    
    async def post(self, url, **kwargs):
        start = time.time()
        try:
            response = await self._client.post(url, **kwargs)
            duration = time.time() - start
            logger.info(f"POST {url} - {response.status_code} - {duration:.3f}s")
            return response
        except Exception as e:
            logger.error(f"POST {url} failed: {e}")
            raise
    
    # Делегируем остальные методы оригинальному клиенту
    def __getattr__(self, name):
        return getattr(self._client, name)

# Использование
client = httpx.AsyncClient()
proxied_client = ApiClientProxy(client)
# Теперь все вызовы через proxied_client логируются автоматически

Декоратор для методов Page Object (UI)

import time
import functools
from playwright.sync_api import Page

def log_and_measure(func):
    """Декоратор для логирования и замера времени методов Page Object."""
    @functools.wraps(func)
    def wrapper(self, *args, **kwargs):
        method_name = func.__name__
        class_name = self.__class__.__name__
        start = time.time()
        
        try:
            result = func(self, *args, **kwargs)
            duration = time.time() - start
            print(f"[{class_name}.{method_name}] completed in {duration:.3f}s")
            return result
        except Exception as e:
            duration = time.time() - start
            print(f"[{class_name}.{method_name}] FAILED after {duration:.3f}s: {e}")
            # Можно добавить скриншот
            if hasattr(self, 'page') and isinstance(self.page, Page):
                self.page.screenshot(path=f"error_{class_name}_{method_name}.png")
            raise
    return wrapper

class LoginPage:
    def __init__(self, page: Page):
        self.page = page
    
    @log_and_measure
    def open(self):
        self.page.goto("https://example.com")
    
    @log_and_measure
    def login(self, username: str, password: str):
        self.page.fill("#username", username)
        self.page.fill("#password", password)
        self.page.click("#login-button")

Универсальный прокси через getattr

class LoggingProxy:
    """Универсальный прокси, логирующий все вызовы."""
    
    def __init__(self, target, name=None):
        self._target = target
        self._name = name or target.__class__.__name__
    
    def __getattr__(self, name):
        attr = getattr(self._target, name)
        
        if callable(attr):
            def wrapped(*args, **kwargs):
                start = time.time()
                try:
                    result = attr(*args, **kwargs)
                    duration = time.time() - start
                    print(f"[{self._name}.{name}] -> {duration:.3f}s")
                    return result
                except Exception as e:
                    duration = time.time() - start
                    print(f"[{self._name}.{name}] ERROR after {duration:.3f}s: {e}")
                    raise
            return wrapped
        return attr

# Использование
page = LoginPage(driver)
proxied_page = LoggingProxy(page, "LoginPage")
proxied_page.open()  # автоматически логируется

Прокси для сбора метрик в Playwright

import { test as base, Page } from '@playwright/test';

// Расширяем объект page прокси для сбора метрик
export const test = base.extend<{ metricsPage: Page }>({
    metricsPage: async ({ page }, use) => {
        const metrics: any[] = [];
        
        // Прокси для перехвата действий
        const proxiedPage = new Proxy(page, {
            get: (target, prop: string) => {
                const value = target[prop as keyof Page];
                
                if (typeof value === 'function') {
                    return async (...args: any[]) => {
                        const start = Date.now();
                        try {
                            const result = await (value as Function).apply(target, args);
                            metrics.push({
                                action: String(prop),
                                duration: Date.now() - start,
                                success: true
                            });
                            return result;
                        } catch (err) {
                            metrics.push({
                                action: String(prop),
                                duration: Date.now() - start,
                                success: false,
                                error: err.message
                            });
                            throw err;
                        }
                    };
                }
                return value;
            }
        });
        
        await use(proxiedPage);
        
        // После теста сохраняем метрики
        console.log('Action metrics:', metrics);
    }
});

Вариативность реализации

  1. Явные прокси-классы – создание классов-обёрток (просто, но много кода).

  2. Декораторы (Python) – элегантный способ добавления функциональности к методам.

  3. Встроенный Proxy (JavaScript) – динамическое создание прокси для любых объектов.

  4. Dynamic Proxy (Java) – для интерфейсов.

  5. Библиотеки (CGLIB, ByteBuddy) – для классов без интерфейсов.

  6. Аспектно-ориентированное программирование (AspectJ) – декларативное добавление сквозной функциональности.

  7. Интерсепторы (axios, okhttp) – встроенные механизмы для HTTP-клиентов.

Когда применять, а когда нет

  • Применять:

    • Для централизованного логирования всех вызовов API или методов Page Object.

    • Для сбора метрик производительности без засорения основного кода.

    • Для добавления retry, таймаутов, обработки ошибок к нестабильным методам.

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

  • Не применять:

    • Для простых объектов с одним-двумя методами (избыточно).

    • Если прокси влияет на производительность (например, в высоконагруженных тестах, но обычно это не критично).

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

Связь с другими паттернами

  • Decorator – очень близкий паттерн, иногда считаются синонимами. Разница: Декоратор фокусируется на добавлении обязанностей, Прокси – на контроле доступа.

  • Interceptor – вариация для перехвата вызовов.

  • AOP – Aspect-Oriented Programming реализует ту же идею на уровне байт-кода.

  • Adapter – может использоваться вместе с прокси для преобразования интерфейсов.

Резюме

Proxy Pattern – мощный инструмент для добавления сквозной функциональности в тестовый фреймворк без изменения основного кода. Он позволяет централизованно управлять логированием, сбором метрик, обработкой ошибок и другими аспектами, соблюдая принцип единственной ответственности. В современных языках есть встроенные средства (декораторы, Proxy, dynamic proxy), которые делают реализацию этого паттерна простой и элегантной. Использование прокси – признак хорошо спроектированной тестовой архитектуры, особенно в крупных проектах с сотнями тестов.

Часть 6. Паттерны расширения и отчётности

(дополнительные возможности, не влияющие на логику тестов)

24. Observer Pattern (подписка на события тестирования)

Название и синонимы

Observer PatternEvent ListenerTest ListenerHookCallbackPublish-Subscribe. В контексте тестовых фреймворков: TestWatcher (JUnit), TestListener (TestNG), pytest hooksPlaywright fixturesAllure Listeners.

Проблема, которую решает

При выполнении тестов часто возникает необходимость выполнять определённые действия на различных этапах жизненного цикла теста:

  • Логирование – запись начала и окончания каждого теста.

  • Скриншоты при ошибке – автоматически делать снимок экрана, если UI-тест упал.

  • Отправка уведомлений – в Slack/Telegram при падении критичных тестов.

  • Сбор метрик – сколько тестов прошло/упало, время выполнения.

  • Интеграция с отчётностью – Allure, ReportPortal.

Если вставлять эту логику прямо в тесты или в фикстуры, код становится перегруженным, дублируется, и его сложно поддерживать. Кроме того, разные аспекты (логирование, отчёты, уведомления) смешиваются, нарушая принцип единственной ответственности.

Суть паттерна

Observer Pattern предлагает создать систему подписчиков (наблюдателей), которые реагируют на события, происходящие в тестовом фреймворке. Тестовый движок (субъект) генерирует события: onTestStartonTestSuccessonTestFailureonTestSkipped и т.д. Наблюдатели подписываются на эти события и выполняют свою логику при их наступлении.

Таким образом, основная логика тестов остаётся чистой, а все побочные действия выносятся в отдельные классы-наблюдатели, которые легко добавлять, удалять и модифицировать независимо друг от друга.

Реализация

1. Python (pytest hooks)

В pytest наблюдатели реализуются через хуки (hooks). Можно создавать плагины или использовать conftest.py для определения функций-обработчиков.

Базовый пример с логированием в conftest.py

# conftest.py
import pytest
import logging
from datetime import datetime

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

def pytest_runtest_setup(item):
    """Вызывается перед каждым тестом."""
    logger.info(f"Starting test: {item.name}")

def pytest_runtest_teardown(item, nextitem):
    """Вызывается после каждого теста."""
    logger.info(f"Finished test: {item.name}")

def pytest_runtest_makereport(item, call):
    """Перехватывает результат выполнения теста."""
    if call.when == "call":
        if call.excinfo is not None:
            logger.error(f"Test failed: {item.name}")
            # Здесь можно сделать скриншот, если есть драйвер
            if hasattr(item, 'funcargs') and 'page' in item.funcargs:
                page = item.funcargs['page']
                screenshot = page.screenshot()
                # прикрепить к отчёту
        else:
            logger.info(f"Test passed: {item.name}")

2. TypeScript (Playwright)

Playwright предоставляет мощную систему слушателей через тестовые хуки и проекционные фикстуры.

Глобальные слушатели в playwright.config.ts

// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  globalSetup: './global-setup.ts',
  globalTeardown: './global-teardown.ts',
  
  // Репортёры (встроенные наблюдатели)
  reporter: [
    ['html'],
    ['json', { outputFile: 'results.json' }],
    ['junit', { outputFile: 'junit.xml' }],
    ['allure-playwright']
  ],
  
  use: {
    // ...
  }
});

3. Java (JUnit5)

В JUnit5 наблюдение реализуется через расширения (extensions) и интерфейсы типа TestWatcherBeforeAllCallbackAfterAllCallback и т.д.

Базовый TestWatcher

import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.TestWatcher;
import java.util.Optional;
import java.util.logging.Logger;

public class LoggingTestWatcher implements TestWatcher {
    private static final Logger logger = Logger.getLogger(LoggingTestWatcher.class.getName());
    
    @Override
    public void testSuccessful(ExtensionContext context) {
        logger.info(String.format("✅ Test passed: %s", context.getDisplayName()));
    }
    
    @Override
    public void testFailed(ExtensionContext context, Throwable cause) {
        logger.severe(String.format("❌ Test failed: %s - %s", 
            context.getDisplayName(), cause.getMessage()));
        
        // Можно добавить скриншот, если есть WebDriver
        takeScreenshot(context);
    }
    
    @Override
    public void testAborted(ExtensionContext context, Throwable cause) {
        logger.warning(String.format("⏸️ Test aborted: %s", context.getDisplayName()));
    }
    
    @Override
    public void testDisabled(ExtensionContext context, Optional<String> reason) {
        logger.info(String.format("⏭️ Test disabled: %s - %s", 
            context.getDisplayName(), reason.orElse("No reason")));
    }
    
    private void takeScreenshot(ExtensionContext context) {
        // Получаем WebDriver из контекста (например, через хранилище)
        WebDriver driver = (WebDriver) context.getStore(ExtensionContext.Namespace.GLOBAL)
                .get("driver");
        if (driver != null) {
            File screenshot = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
            // Сохраняем или прикрепляем к отчёту
        }
    }
}

Использование расширения в тесте

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

@ExtendWith(LoggingTestWatcher.class)
class LoginTest {
    
    @Test
    void testSuccessfulLogin() {
        // тест
    }
    
    @Test
    void testFailedLogin() {
        // тест
    }
}

TestNG (альтернатива)

import org.testng.ITestContext;
import org.testng.ITestListener;
import org.testng.ITestResult;

public class CustomTestListener implements ITestListener {
    
    @Override
    public void onTestStart(ITestResult result) {
        System.out.println("Test started: " + result.getName());
    }
    
    @Override
    public void onTestSuccess(ITestResult result) {
        System.out.println("Test passed: " + result.getName());
    }
    
    @Override
    public void onTestFailure(ITestResult result) {
        System.out.println("Test failed: " + result.getName());
        // скриншот, уведомление
    }
    
    @Override
    public void onTestSkipped(ITestResult result) {
        System.out.println("Test skipped: " + result.getName());
    }
}

Вариативность реализации

  1. Встроенные слушатели тестовых фреймворков – JUnit5 TestWatcher, TestNG ITestListener, pytest hooks.

  2. Репортёры – специализированные наблюдатели для формирования отчётов (Allure, HTML, JSON).

  3. Кастомные расширения – создание собственных слушателей под конкретные нужды.

  4. Аспектно-ориентированное программирование – AOP для перехвата событий на более низком уровне.

  5. Шина событий (Event Bus) – создание глобальной шины, в которую можно публиковать события и подписывать различные обработчики.

Когда применять, а когда нет

  • Применять:

    • Для централизованного логирования всех тестов.

    • Для автоматического создания скриншотов/видео при падениях.

    • Для сбора метрик и статистики выполнения.

    • Для интеграции с внешними системами (Allure, ReportPortal, CI).

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

  • Не применять:

    • Если логика тесно связана с конкретным тестом и не переиспользуется.

    • В очень маленьких проектах с 5-10 тестами (избыточно).

Связь с другими паттернами

  • Proxy Pattern – близок, но прокси перехватывает вызовы методов объектов, а Observer реагирует на события тестового движка.

  • Decorator – может использоваться для добавления наблюдателей.

  • Event-Driven Architecture – Observer является частным случаем событийно-ориентированной архитектуры.

Резюме

Observer Pattern – фундаментальный паттерн для отделения побочной логики (логирование, отчёты, уведомления) от основной логики тестов. Он позволяет создавать гибкие и расширяемые тестовые фреймворки, где новые наблюдатели можно добавлять без изменения существующего кода. Современные тестовые инструменты (pytest, JUnit, Playwright) предоставляют встроенную поддержку этого паттерна через хуки, слушатели и расширения, что делает его реализацию простой и естественной. Использование Observer Pattern – признак хорошо архитектурно продуманного тестового проекта.

25. Template Method Pattern (шаблон тестового класса)

Название и синонимы

Template Method PatternШаблонный методБазовый класс тестаAbstract Test Class.

Проблема, которую решает

Многие тесты имеют одинаковую структуру, но различаются деталями реализации отдельных шагов. Например: открыть страницу → авторизоваться → выполнить действие → проверить результат. Без шаблона приходится дублировать код структуры в каждом тесте, а изменение общего алгоритма требует правки всех тестов.

Суть паттерна

Template Method определяет скелет алгоритма в базовом классе, оставляя реализацию отдельных шагов подклассам. Базовый класс может предоставлять реализации по умолчанию для опциональных шагов.

Краткие примеры

Java (JUnit5)

abstract class BaseApiTest {
    
    @BeforeEach
    void setUp() {
        client = createClient();
        authenticate();
    }
    
    abstract ApiClient createClient();
    abstract void authenticate();
    
    // Шаблонный метод
    void testResourceCreation(Resource data) {
        Resource created = createResource(data);
        validateResource(created);
        cleanupResource(created);
    }
    
    abstract Resource createResource(Resource data);
    abstract void validateResource(Resource resource);
    abstract void cleanupResource(Resource resource);
}

class UserApiTest extends BaseApiTest {
    @Override ApiClient createClient() { return new ApiClient(); }
    @Override void authenticate() { client.login("user", "pass"); }
    @Override Resource createResource(Resource data) { return client.post("/users", data); }
    @Override void validateResource(Resource r) { assertNotNull(r.getId()); }
    @Override void cleanupResource(Resource r) { client.delete("/users/" + r.getId()); }
    
    @Test
    void testCreateUser() {
        testResourceCreation(new User("john"));
    }
}

Python (pytest)

class BaseUiTest:
    @pytest.fixture(autouse=True)
    def setup(self, page):
        self.page = page
        self.open_base_url()
    
    @abstractmethod
    def open_base_url(self): pass
    
    def test_login_template(self, username, password, should_succeed):
        self.enter_credentials(username, password)
        self.submit_login()
        self.verify_result(should_succeed)
    
    @abstractmethod
    def enter_credentials(self, username, password): pass
    @abstractmethod
    def submit_login(self): pass
    @abstractmethod
    def verify_result(self, should_succeed): pass

class TestSauceDemo(BaseUiTest):
    def open_base_url(self):
        self.page.goto("https://saucedemo.com")
    
    def enter_credentials(self, username, password):
        self.page.fill("#user-name", username)
        self.page.fill("#password", password)
    
    def submit_login(self):
        self.page.click("#login-button")
    
    def verify_result(self, should_succeed):
        if should_succeed:
            expect(self.page).to_have_url(/inventory/)
        else:
            expect(self.page.locator("[data-test='error']")).to_be_visible()
    
    def test_successful_login(self):
        self.test_login_template("standard_user", "secret_sauce", True)

Когда применять

  • Есть повторяющаяся структура тестов с вариативной реализацией шагов.

  • Хочется централизовать общую логику (логирование, замер времени, обработка ошибок).

  • Нужно гарантировать, что все тесты определённой группы выполняют одинаковую последовательность действий.

Связь с другими паттернами

  • Factory Method – часто используется внутри Template Method для создания объектов.

  • Strategy – если нужно менять алгоритм целиком, а не отдельные шаги.

  • Hook Methods – опциональные шаги с пустой реализацией по умолчанию.

Часть 7. Специфические паттерны для UI (визуальное и кросс-браузерное тестирование)

26. Visual Testing (визуальное сравнение)

Название и синонимы

Visual TestingVisual Regression TestingScreenshot TestingВизуальное регрессионное тестирование. Инструменты: Playwright ScreenshotsPercyApplitoolsLokiBackstopJS.

Проблема, которую решает

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

Суть паттерна

Visual Testing автоматически сравнивает скриншоты текущего состояния интерфейса с эталонными. При первом запуске создаются baseline-скриншоты. При последующих запусках тест сравнивает текущий скриншот с эталоном и падает при различиях. Разработчик либо исправляет код, либо обновляет эталон (если изменения ожидаемые).

Ключевые возможности:

  • Маскирование динамических областей – скрытие дат, цен, анимаций, которые не должны влиять на сравнение.

  • Допуск (threshold) – игнорирование незначительных различий (например, из-за антиалиасинга).

  • Кросс-браузерное сравнение – проверка, что интерфейс одинаково выглядит во всех браузерах.

Реализация

1. Playwright (TypeScript)

Базовый пример со скриншотом всей страницы

import { test, expect } from '@playwright/test';

test('homepage screenshot', async ({ page }) => {
  await page.goto('https://example.com');
  await expect(page).toHaveScreenshot('homepage.png');
});

С маскированием динамических элементов

test('inventory page with masked prices', async ({ page }) => {
  await page.goto('https://www.saucedemo.com/inventory.html');
  
  await expect(page).toHaveScreenshot('inventory-page.png', {
    mask: [
      page.locator('.inventory_item_price'), // маскируем цены
      page.locator('.shopping_cart_badge')   // маскируем счётчик корзины
    ],
    maskColor: '#f0f0f0' // цвет маски (по умолчанию #f00 с прозрачностью)
  });
});

С настройкой допуска и анимаций

test('with animation and threshold', async ({ page }) => {
  await page.goto('https://example.com');
  
  await expect(page).toHaveScreenshot('animated-element.png', {
    animations: 'disabled', // отключаем анимации
    threshold: 0.2,         // допуск 0.2% различающихся пикселей
    maxDiffPixels: 100,     // максимальное число различающихся пикселей
    timeout: 10000
  });
});

Для конкретного элемента

test('element screenshot', async ({ page }) => {
  await page.goto('https://example.com');
  const header = page.locator('header');
  await expect(header).toHaveScreenshot('header.png');
});

Обновление эталонов

# Обновить все скриншоты
npx playwright test --update-snapshots

# Обновить скриншоты для конкретного теста
npx playwright test login --update-snapshots

2. Playwright (Python)

from playwright.sync_api import expect

def test_homepage(page):
    page.goto("https://example.com")
    expect(page).to_have_screenshot("homepage.png")

def test_with_masking(page):
    page.goto("https://www.saucedemo.com/inventory.html")
    expect(page).to_have_screenshot(
        "inventory.png",
        mask=[page.locator(".inventory_item_price")],
        mask_color="#f0f0f0"
    )

Вариативность реализации

  1. Локальное хранение эталонов (Playwright) – просто, но эталоны занимают место в репозитории.

  2. Облачные сервисы (Percy, Applitools) – эталоны хранятся в облаке, удобно для команд, есть веб-интерфейс для утверждения изменений.

  3. Маскирование – скрытие динамических областей (цены, даты, ID).

  4. Регионы игнорирования – прямоугольные области, которые исключаются из сравнения.

  5. Пороговые значения – процент различий или количество пикселей.

  6. Кросс-браузерные скриншоты – сравнение, как страница выглядит в разных браузерах.

Когда применять, а когда нет

  • Применять:

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

    • При частых изменениях вёрстки (регрессионное тестирование).

    • Для компонентов, где важен внешний вид (кнопки, формы, карточки).

  • Не применять:

    • Для страниц с постоянно меняющимся контентом (ленты новостей, курсы валют) без маскирования.

    • Если тесты выполняются на разных ОС/браузерах и различия неизбежны (тогда нужен допуск или отдельные эталоны).

Связь с другими паттернами

  • Snapshot Testing – визуальное тестирование является частным случаем снэпшот-тестирования для изображений.

  • Page Object – скриншоты обычно делаются через объекты страниц.

  • Observer Pattern – можно автоматически делать скриншоты при падении тестов.

Резюме

Visual Testing – незаменимый инструмент для обнаружения визуальных регрессий, которые не ловят функциональные тесты. Современные инструменты (Playwright, Percy, Applitools) делают его простым в интеграции и использовании. Важно правильно маскировать динамические области и настраивать допуски, чтобы избежать ложных падений. В сочетании с функциональными тестами визуальное тестирование даёт полную уверенность в качестве интерфейса.

27. Cross-Browser & Mobile Testing

Название и синонимы

Cross-Browser TestingCross-Platform TestingMobile TestingКросс-браузерное тестированиеТестирование на мобильных устройствах.

Проблема, которую решает

Пользователи заходят на сайт с разных браузеров (Chrome, Firefox, Safari, Edge) и устройств (десктопы, планшеты, смартфоны). Каждый браузер по-своему интерпретирует CSS и JavaScript, мобильные устройства имеют разные разрешения и особенности ввода. Если тестировать только в одном браузере, можно пропустить критические баги, которые проявятся у реальных пользователей.

Суть паттерна

Cross-Browser & Mobile Testing – это запуск одних и тех же тестов в разных браузерах и на разных устройствах (реальных или эмулируемых). Цель – убедиться, что приложение работает корректно во всех целевых окружениях.

Современные фреймворки (Playwright, Selenium) позволяют легко настроить запуск тестов параллельно в нескольких конфигурациях.

Реализация

1. Playwright (TypeScript)

Настройка проектов в playwright.config.ts

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  projects: [
    // Десктопные браузеры
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
    {
      name: 'edge',
      use: { ...devices['Desktop Edge'] },
    },
    
    // Мобильные устройства
    {
      name: 'mobile-chrome',
      use: { ...devices['Pixel 5'] },
    },
    {
      name: 'mobile-safari',
      use: { ...devices['iPhone 12'] },
    },
    {
      name: 'tablet',
      use: { ...devices['iPad Pro 11'] },
    },
    
    // С эмуляцией геолокации и ориентации
    {
      name: 'mobile-landscape',
      use: {
        ...devices['iPhone 12'],
        viewport: { width: 844, height: 390 },
        deviceScaleFactor: 3,
      },
    },
  ],
});

Условная логика в тестах

test('should work on all browsers', async ({ page, browserName }) => {
  test.skip(browserName === 'webkit', 'WebKit has issues with this feature');
  
  if (browserName === 'firefox') {
    // специфичная логика для Firefox
  }
  
  // основной тест
});

2. Playwright (Python)

# playwright.config.py
from playwright.sync_api import sync_playwright

config = {
    "projects": [
        {"name": "chromium", "use": {"channel": "chrome"}},
        {"name": "firefox", "use": {}},
        {"name": "webkit", "use": {}},
        {"name": "mobile", "use": {"device": "iPhone 12"}},
    ]
}

3. Selenium (Java)

TestNG с параметрами браузеров

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.edge.EdgeDriver;
import org.testng.annotations.Parameters;
import org.testng.annotations.Test;

public class CrossBrowserTest {
    WebDriver driver;
    
    @Parameters("browser")
    @Test
    public void testOnDifferentBrowsers(String browser) {
        if (browser.equalsIgnoreCase("chrome")) {
            driver = new ChromeDriver();
        } else if (browser.equalsIgnoreCase("firefox")) {
            driver = new FirefoxDriver();
        } else if (browser.equalsIgnoreCase("edge")) {
            driver = new EdgeDriver();
        }
        
        driver.get("https://example.com");
        // тест
        driver.quit();
    }
}

testng.xml

<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="Cross-Browser Suite" parallel="tests">
    <test name="Chrome Test">
        <parameter name="browser" value="chrome"/>
        <classes>
            <class name="CrossBrowserTest"/>
        </classes>
    </test>
    <test name="Firefox Test">
        <parameter name="browser" value="firefox"/>
        <classes>
            <class name="CrossBrowserTest"/>
        </classes>
    </test>
    <test name="Edge Test">
        <parameter name="browser" value="edge"/>
        <classes>
            <class name="CrossBrowserTest"/>
        </classes>
    </test>
</suite>

Вариативность реализации

  1. Эмуляция устройств (Playwright, Chrome DevTools) – быстро, не требует реальных устройств.

  2. Реальные устройства (BrowserStack, Sauce Labs, LambdaTest) – максимально приближено к реальности, но платно.

  3. Локальные браузеры – запуск на установленных браузерах.

  4. Контейнеры (Selenium Grid, Docker) – для масштабирования и параллельного запуска.

  5. Облачные фермы – доступ к сотням комбинаций браузеров и ОС.

Когда применять, а когда нет

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

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

  • Не применять для внутренних корпоративных приложений с фиксированным окружением (например, только Chrome).

Связь с другими паттернами

  • Parallel Execution – кросс-браузерные тесты обычно запускаются параллельно.

  • Configuration Pattern – настройки браузеров выносятся в конфигурационные файлы.

  • Visual Testing – визуальные тесты должны запускаться во всех целевых браузерах.

Резюме

Cross-Browser & Mobile Testing – обязательная практика для веб-приложений, ориентированных на широкую аудиторию. Современные инструменты (Playwright, Selenium Grid, облачные платформы) позволяют автоматизировать этот процесс и запускать тесты параллельно на десятках комбинаций браузеров и устройств. Правильная стратегия – выбрать репрезентативный набор окружений (основные браузеры, популярные мобильные устройства) и интегрировать кросс-браузерные тесты в CI/CD.

Часть 8. Дополнительные паттерны (общие)

28. Singleton (одиночка)

Название и синонимы

SingletonОдиночкаЕдинственный экземпляр.

Проблема, которую решает

В тестовом фреймворке часто нужны объекты, которые должны существовать в единственном экземпляре: конфигурация, менеджер ресурсов (пул соединений), логгер, кеш токенов. Если создавать несколько экземпляров, возникает несогласованность данных, избыточное потребление ресурсов или конфликты доступа.

Суть паттерна

Singleton гарантирует, что класс имеет только один экземпляр, и предоставляет глобальную точку доступа к нему.

Примеры

Python

class AppConfig:
    _instance = None
    
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance._load_config()
        return cls._instance
    
    def _load_config(self):
        # загружаем конфиг один раз
        self.base_url = "https://api.example.com"
        self.timeout = 30

# Использование
config1 = AppConfig()
config2 = AppConfig()
assert config1 is config2  # один и тот же объект

TypeScript

class AppConfig {
    private static instance: AppConfig;
    public readonly baseUrl: string;
    public readonly timeout: number;
    
    private constructor() {
        this.baseUrl = process.env.BASE_URL || 'https://api.example.com';
        this.timeout = parseInt(process.env.TIMEOUT || '30');
    }
    
    static getInstance(): AppConfig {
        if (!AppConfig.instance) {
            AppConfig.instance = new AppConfig();
        }
        return AppConfig.instance;
    }
}

// Использование
const config = AppConfig.getInstance();

Java

public class AppConfig {
    private static AppConfig instance;
    private String baseUrl;
    private int timeout;
    
    private AppConfig() {
        this.baseUrl = System.getenv().getOrDefault("BASE_URL", "https://api.example.com");
        this.timeout = Integer.parseInt(System.getenv().getOrDefault("TIMEOUT", "30"));
    }
    
    public static synchronized AppConfig getInstance() {
        if (instance == null) {
            instance = new AppConfig();
        }
        return instance;
    }
}

Когда применять

  • Для конфигурации, загружаемой один раз.

  • Для менеджеров ресурсов (пулы соединений, HTTP-клиенты).

  • Для кешей (токены, тестовые данные).

  • Для логгеров и репортёров.

Связь с другими паттернами

  • Factory – может возвращать синглтон.

  • Dependency Injection – DI-контейнеры часто реализуют объекты как синглтоны (scope=singleton).

29. Command Pattern (команда)

Название и синонимы

CommandКомандаActionTransaction.

Проблема, которую решает

В сложных тестовых сценариях нужно выполнять последовательности действий, иногда с возможностью отката (undo), логирования, постановки в очередь. Если действия жёстко зашиты в код, их трудно переиспользовать, комбинировать и отменять.

Суть паттерна

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

Примеры

TypeScript

interface Command {
    execute(): void;
    undo(): void;
}

class AddToCartCommand implements Command {
    constructor(private cart: Cart, private product: string) {}
    
    execute() { this.cart.add(this.product); }
    undo() { this.cart.remove(this.product); }
}

// Использование в тесте
test('shopping cart macro', () => {
    const cart = new Cart();
    const commands = [
        new AddToCartCommand(cart, 'laptop'),
        new AddToCartCommand(cart, 'mouse'),
    ];
    
    commands.forEach(cmd => cmd.execute());
    expect(cart.total).toBe(2);
    
    // Отмена
    commands.reverse().forEach(cmd => cmd.undo());
    expect(cart.total).toBe(0);
});

Когда применять

  • Для сложных сценариев с возможностью отката.

  • Для построения макросов из повторяющихся действий.

  • Для логирования истории действий.

  • Для реализации пошаговых тестов (wizard, checkout).

30. Strategy Pattern (стратегия)

Название и синонимы

StrategyСтратегияPolicy.

Проблема, которую решает

В тестах часто нужны разные алгоритмы для одной и той же задачи: генерация тестовых данных (случайные / фиксированные / из файла), способы логина (через UI / через API), стратегии ожидания (фиксированное / экспоненциальное). Если использовать условные операторы (if/else), код становится сложным для расширения.

Суть паттерна

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

Примеры

Python

from abc import ABC, abstractmethod

class DataGenerationStrategy(ABC):
    @abstractmethod
    def generate_user(self): pass

class RandomDataStrategy(DataGenerationStrategy):
    def generate_user(self):
        return {
            "username": faker.user_name(),
            "email": faker.email(),
            "password": faker.password()
        }

class FixedDataStrategy(DataGenerationStrategy):
    def generate_user(self):
        return {
            "username": "testuser",
            "email": "test@example.com",
            "password": "password123"
        }

class TestDataGenerator:
    def __init__(self, strategy: DataGenerationStrategy):
        self._strategy = strategy
    
    def set_strategy(self, strategy: DataGenerationStrategy):
        self._strategy = strategy
    
    def create_user(self):
        return self._strategy.generate_user()

# Использование
generator = TestDataGenerator(RandomDataStrategy())
user1 = generator.create_user()  # случайный

generator.set_strategy(FixedDataStrategy())
user2 = generator.create_user()  # фиксированный

TypeScript

interface AuthStrategy {
    login(page: Page): Promise<void>;
}

class UiAuthStrategy implements AuthStrategy {
    async login(page: Page): Promise<void> {
        await page.fill('#username', 'user');
        await page.fill('#password', 'pass');
        await page.click('#login');
    }
}

class ApiAuthStrategy implements AuthStrategy {
    async login(page: Page): Promise<void> {
        const token = await this.getTokenViaApi();
        await page.evaluate(`localStorage.setItem('token', '${token}')`);
    }
    
    private async getTokenViaApi(): Promise<string> {
        // вызов API для получения токена
    }
}

class LoginContext {
    constructor(private strategy: AuthStrategy) {}
    
    setStrategy(strategy: AuthStrategy) {
        this.strategy = strategy;
    }
    
    async login(page: Page): Promise<void> {
        await this.strategy.login(page);
    }
}

// Использование
const login = new LoginContext(new UiAuthStrategy());
await login.login(page);  // логин через UI

login.setStrategy(new ApiAuthStrategy());
await login.login(page);  // логин через API

Когда применять

  • Когда есть несколько вариантов алгоритма (генерация данных, аутентификация, расчёт).

  • Когда нужно переключать поведение во время выполнения.

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

Связь с другими паттернами

  • Factory – может создавать стратегии.

  • Builder – может использовать разные стратегии построения.

  • Template Method – если нужно фиксировать структуру, но менять шаги, то Template Method; если нужна полная замена алгоритма – Strategy.

Часть 9. Антипаттерны в автоматизации тестирования

(распространённые ошибки, их вред и как их избежать)

Антипаттерны — это распространённые ошибки, которые делают тесты хрупкими, медленными, сложными в поддержке и ненадёжными. Их знание помогает вовремя распознать проблему и применить правильное решение.


1. Hardcoded test data (жёстко зашитые данные)

Суть: В тестах используются конкретные значения: "user@test.com""password123"id=42.
Вред: При изменении данных (например, пользователь удалён) тесты падают. Нельзя запускать параллельно — конфликты из-за одинаковых данных.
Решение: Выносить данные в константы, использовать фабрики (Factory), генераторы (Faker), параметризацию.


2. Lack of isolation (отсутствие изоляции)

Суть: Тесты работают с общим состоянием (БД, файлы, кеш) и влияют друг на друга.
Вред: Порядок запуска влияет на результат, параллельный запуск невозможен, тесты flaky.
Решение: Transactional Tests (откат транзакций), очистка данных перед/после теста (TRUNCATEDELETE), использование уникальных идентификаторов.


3. Sleeps instead of waits (ожидания через sleep)

Суть: Использование фиксированных пауз Thread.sleep(5000) вместо ожидания условий.
Вред: Тесты медленные (всегда ждут максимум) и flaky (если операция занимает больше времени).
Решение: Использовать явные ожидания (WebDriverWait, expect.pollwait_for_condition) с проверкой состояния.


4. Over-mocking (избыточное мокирование)

Суть: Мокируются все внешние зависимости, включая стабильные и простые.
Вред: Тесты теряют связь с реальностью, могут проходить, но реальная система сломана. Моки расходятся с реальным API.
Решение: Мокировать только нестабильные/медленные/внешние сервисы, для остальных использовать реальные реализации или тестовые контейнеры.


5. Page Object как склад селекторов

Суть: Page Object содержит только локаторы, а тесты напрямую работают с ними: page.locator(loginPage.usernameInput).fill().
Вред: Детали реализации просачиваются в тесты, при изменении селектора нужно править все тесты, а не один метод.
Решение: Добавлять бизнес-методы (loginPage.login(user, pass)), скрывающие селекторы.


6. Duplicate code (дублирование кода)

Суть: Одинаковые шаги повторяются в разных тестах (логин, создание заказа, очистка).
Вред: Увеличение объёма тестов, сложность поддержки (правка в N местах).
Решение: Выносить повторяющуюся логику в фикстуры, хелперы, компоненты, базовые классы.


7. Flaky tests без анализа и retries (нестабильные тесты)

Суть: Тесты то падают, то проходят, но на это не обращают внимания или просто добавляют retry без анализа.
Вред: Снижение доверия к тестам, реальные баги могут быть пропущены.
Решение: Анализировать причины flakiness, исправлять их, использовать retry только как временную меру с логированием.


8. Too much logic in tests (избыточная логика в тестах)

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


9. No version control for test data (тестовые данные не под версионным контролем)

Суть: Данные в БД или тестовых файлах меняются независимо от кода тестов.
Вред: Тесты начинают падать после изменения данных, непонятно, что пошло не так.
Решение: Хранить тестовые данные в репозитории (SQL-скрипты, JSON-файлы), использовать миграции или фабрики, генерирующие данные на лету.


10. Ignoring test failures (игнорирование падений)

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


11. Неверный выбор паттерна

Суть: Применение сложного паттерна там, где он не нужен (например, POM для одной кнопки, Builder для двух полей).
Вред: Избыточная сложность, оверхед, трудности понимания.
Решение: Выбирать паттерн по необходимости, начинать с простого решения, усложнять только когда появляется реальная потребность.


12. Длинные тесты (Long tests)

Суть: Один тест проверяет слишком много шагов и сценариев.
Вред: При падении сложно понять, что именно сломалось, тесты медленные, их трудно поддерживать.
Решение: Разбивать на несколько небольших тестов по принципу "один тест — одна проверка", использовать параметризацию для разных данных.

Заключение

Мы рассмотрели около 30 паттернов автоматизации тестирования — от фундаментальных (Page Object, API Client) до продвинутых (Pact, Circuit Breaker, Property-Based Testing). Но важно помнить: паттерны — это не догма, а инструменты. Их цель — решать реальные проблемы, а не украшать код.

Как выбирать паттерны под проект?

Начинайте с малого. Для UI-тестов достаточно Page Object и базовых фикстур. Для API — клиентской обёртки и параметризации. Когда тестов становится больше, вы столкнётесь с дублированием — внедряйте Factory/Builder. Появятся проблемы стабильности — подключайте WaiterRetryTransactional Tests. Начнут расти время прогона — используйте Parallel Execution и кэширование тяжёлых ресурсов. Интеграция с внешними сервисами потребует API Mocking или Contract Testing.

Не усложняйте преждевременно. Если проект живёт месяц и имеет 10 тестов — не нужен Pact или собственный мок-сервер. Но когда боль становится ощутимой — паттерн предложит проверенное решение.

Паттерны — не догма

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

  • Надёжными (не падают без причины);

  • Быстрыми (дают обратную связь за минуты);

  • Понятными (их может дополнить любой член команды);

  • Поддерживаемыми (изменения в продукте требуют минимальных правок).