Функциональные паттерны для тестирования
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 лаконичны |
| Предварительно сконфигурированные утилиты | Замыкания | Захват конфигурации, возврат сфокусированных функций |
| Генерация тестовых данных | Фабрики (замыкания) | Уникальные данные с единообразными значениями по умолчанию |
| Обработка результатов тестов | Функциональный конвейер | Каждый шаг независим и тестируем |
| Сложная подготовка тестов | ООП (фикстуры) | Управление жизненным циклом состояния |
Практическое упражнение
- Напишите фабрику API-клиента на основе замыканий, поддерживающую разные методы аутентификации (токен, API-ключ, basic auth)
- Напишите фабрику тестовых данных, генерирующую уникальных пользователей, заказы и продукты
- Обработайте список ответов API с помощью функционального конвейера: отфильтруйте ошибки, извлеките сообщения об ошибках, сгруппируйте по коду ошибки, сгенерируйте сводный отчёт
- Рефакторьте существующий тест с for-циклом в функциональный стиль с использованием map/filter
Ключевые выводы
- map/filter/reduce заменяют явные циклы декларативной обработкой данных
- Замыкания создают предварительно сконфигурированные функции — необходимы для API-клиентов и фабрик
- Функции высшего порядка создают гибкие, переиспользуемые тестовые утилиты
- Предпочитайте неизменяемые данные при подготовке тестов для предотвращения ошибок общего состояния
- Комбинируйте функциональные паттерны (обработка данных) с ООП (структура фреймворка)