Modern QA2026Функциональные паттерны для тестирования
Join

Course12 Programming for QA

Foundations · Chapter 12

Функциональные паттерны для тестирования

Updated Jul 2026

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

Map, Filter и Reduce

Эти три операции составляют основу функциональной обработки данных. Они заменяют явные циклы декларативными преобразованиями.

Python

# map: transform each item
responses = [requests.get(f"{base_url}/users/{i}") for i in range(1, 11)]
status_codes = list(map(lambda r: r.status_code, responses))
# [200, 200, 404, 200, 500, ...]

# filter: keep items matching a condition
errors = list(filter(lambda r: r.status_code >= 500, responses))
assert len(errors) == 0, f"Server errors found: {[r.url for r in errors]}"

# reduce: aggregate into a single value
from functools import reduce
total_time = reduce(lambda acc, r: acc + r.elapsed.total_seconds(), responses, 0)
print(f"Total request time: {total_time:.2f}s")

# Pythonic alternatives (often preferred)
status_codes = [r.status_code for r in responses]          # list comprehension > map
errors = [r for r in responses if r.status_code >= 500]    # comprehension > filter
total_time = sum(r.elapsed.total_seconds() for r in responses)  # sum > reduce

JavaScript/TypeScript

const responses = await Promise.all(
    Array.from({ length: 10 }, (_, i) => fetch(`${baseUrl}/users/${i + 1}`))
);

// map
const codes = responses.map(r => r.status);
// [200, 200, 404, 200, 500, ...]

// filter
const errors = responses.filter(r => r.status >= 500);
expect(errors).toHaveLength(0);

// reduce
const totalTime = responses.reduce((acc, r) => acc + r.duration, 0);

// Chaining: filter, then map, then reduce
const errorMessages = responses
    .filter(r => r.status >= 400)
    .map(r => `${r.status}: ${r.url}`)
    .join("\n");

Замыкания

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

Фабрика API-клиента

def make_api_client(base_url: str, token: str):
    """Creates a pre-configured API client."""
    def request(method: str, path: str, **kwargs):
        headers = {"Authorization": f"Bearer {token}"}
        headers.update(kwargs.pop("headers", {}))
        return requests.request(method, f"{base_url}{path}", headers=headers, **kwargs)
    return request

# Usage: create clients for different environments
staging_api = make_api_client("https://api.staging.example.com", os.environ["STAGING_TOKEN"])
prod_api = make_api_client("https://api.example.com", os.environ["PROD_TOKEN"])

# Both use the same interface
staging_response = staging_api("GET", "/users/me")
prod_response = prod_api("GET", "/users/me")

Обёртка для повторных попыток

def with_retry(max_attempts: int = 3, delay: float = 1.0):
    """Creates a retry wrapper for flaky operations."""
    def decorator(func):
        def wrapper(*args, **kwargs):
            last_exception = None
            for attempt in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    last_exception = e
                    if attempt < max_attempts - 1:
                        time.sleep(delay * (attempt + 1))
            raise last_exception
        return wrapper
    return decorator

@with_retry(max_attempts=3, delay=0.5)
def fetch_user(user_id: str):
    response = requests.get(f"{base_url}/users/{user_id}")
    response.raise_for_status()
    return response.json()

Замыкания в TypeScript

function makeApiClient(baseUrl: string, token: string) {
    return async (method: string, path: string, body?: object) => {
        const response = await fetch(`${baseUrl}${path}`, {
            method,
            headers: {
                Authorization: `Bearer ${token}`,
                "Content-Type": "application/json",
            },
            body: body ? JSON.stringify(body) : undefined,
        });
        return response;
    };
}

const api = makeApiClient("https://api.staging.example.com", process.env.TOKEN!);
const response = await api("GET", "/users/me");

Функции высшего порядка

Функция высшего порядка принимает функцию в качестве аргумента или возвращает функцию. Они являются строительными блоками гибких тестовых утилит.

Конструктор пользовательских проверок

def assert_response(response, status=None, contains=None, excludes=None):
    """Flexible response assertion."""
    if status is not None:
        assert response.status_code == status, \
            f"Expected {status}, got {response.status_code}: {response.text[:200]}"

    if contains is not None:
        body = response.json()
        for key in contains:
            assert key in body, f"Response missing field: {key}"

    if excludes is not None:
        body = response.json()
        for key in excludes:
            assert key not in body, f"Response contains forbidden field: {key}"

# Usage
assert_response(r, status=200, contains=["id", "name"], excludes=["password"])

Генератор тестовых данных

def make_user_factory(defaults: dict):
    """Creates a factory that generates user data with overrides."""
    counter = 0
    def create(**overrides):
        nonlocal counter
        counter += 1
        user = {**defaults, **overrides}
        user["email"] = user.get("email", f"testuser{counter}@test.com")
        return user
    return create

create_user = make_user_factory({"role": "viewer", "active": True})

# Each call creates a unique user with defaults
user1 = create_user()                          # testuser1@test.com, viewer, active
user2 = create_user(role="admin")              # testuser2@test.com, admin, active
user3 = create_user(email="custom@test.com")   # custom@test.com, viewer, active

Неизменяемость

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

# BAD: mutable default — all tests share the same list
def create_order(items=[]):  # list is created once and shared!
    items.append("new_item")
    return items

# GOOD: immutable approach
def create_order(items=None):
    items = list(items or [])  # create a new list each time
    items.append("new_item")
    return items

# BEST: use frozen dataclass for test data
from dataclasses import dataclass

@dataclass(frozen=True)
class TestUser:
    email: str
    password: str
    role: str = "viewer"

admin = TestUser("admin@test.com", "pass123", "admin")
# admin.role = "viewer"  # This raises FrozenInstanceError

Паттерн конвейера (Pipe/Chain)

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

# Process test results through a pipeline
def load_results(path):
    with open(path) as f:
        return json.load(f)

def filter_failures(results):
    return [r for r in results if r["status"] == "FAIL"]

def enrich_with_duration(results):
    return [{**r, "slow": r["duration"] > 5.0} for r in results]

def format_report(results):
    return "\n".join(f"FAIL: {r['name']} ({r['duration']}s)" for r in results)

# Pipeline
results = load_results("results.json")
report = format_report(enrich_with_duration(filter_failures(results)))
// TypeScript: chaining with array methods
const report = testResults
    .filter(r => r.status === "FAIL")
    .map(r => ({ ...r, slow: r.duration > 5.0 }))
    .map(r => `FAIL: ${r.name} (${r.duration}s)${r.slow ? " [SLOW]" : ""}`)
    .join("\n");

Когда использовать функциональный подход и когда ООП

Ситуация Подход Почему
Объекты страниц, структура фреймворка ООП Управление состоянием, инкапсуляция
Преобразование данных в проверках Функциональный map/filter/reduce лаконичны
Предварительно сконфигурированные утилиты Замыкания Захват конфигурации, возврат сфокусированных функций
Генерация тестовых данных Фабрики (замыкания) Уникальные данные с единообразными значениями по умолчанию
Обработка результатов тестов Функциональный конвейер Каждый шаг независим и тестируем
Сложная подготовка тестов ООП (фикстуры) Управление жизненным циклом состояния

Практическое упражнение

  1. Напишите фабрику API-клиента на основе замыканий, поддерживающую разные методы аутентификации (токен, API-ключ, basic auth)
  2. Напишите фабрику тестовых данных, генерирующую уникальных пользователей, заказы и продукты
  3. Обработайте список ответов API с помощью функционального конвейера: отфильтруйте ошибки, извлеките сообщения об ошибках, сгруппируйте по коду ошибки, сгенерируйте сводный отчёт
  4. Рефакторьте существующий тест с for-циклом в функциональный стиль с использованием map/filter

Ключевые выводы

  • map/filter/reduce заменяют явные циклы декларативной обработкой данных
  • Замыкания создают предварительно сконфигурированные функции — необходимы для API-клиентов и фабрик
  • Функции высшего порядка создают гибкие, переиспользуемые тестовые утилиты
  • Предпочитайте неизменяемые данные при подготовке тестов для предотвращения ошибок общего состояния
  • Комбинируйте функциональные паттерны (обработка данных) с ООП (структура фреймворка)