Реализовывал ли собственный декоратор
Комментарии (2)
Ответ сгенерирован нейросетью и может содержать ошибки
Да, многократно реализовывал собственные декораторы
За годы работы с Python в контексте QA Automation, создание собственных декораторов стало рутинной, но мощной практикой для решения широкого круга задач: от улучшения стабильности тестов и логирования до управления временем выполнения и интеграции с системами отчетности. Декораторы — это элегантный инструмент метапрограммирования, позволяющий инкапсулировать сквозную функциональность (cross-cutting concerns) без модификации исходного кода тестовых методов.
Практические примеры из опыта автоматизации
1. Декоратор для логирования и трассировки выполнения
Этот декоратор фиксирует начало, конец, аргументы и результат вызова функции, что незаменимо при отладке сложных тестовых сценариев или анализе падений.
import functools
import logging
def log_execution(func):
"""
Декоратор для логирования входа, выхода и ошибок функции.
"""
@functools.wraps(func) # Сохраняем метаданные оригинальной функции
def wrapper(*args, **kwargs):
logger = logging.getLogger(func.__module__)
# Логируем начало выполнения
logger.info(f"Выполнение {func.__name__} с args={args}, kwargs={kwargs}")
try:
result = func(*args, **kwargs)
# Логируем успешное завершение
logger.info(f"Функция {func.__name__} завершилась успешно. Результат: {result}")
return result
except Exception as e:
# Логируем исключение с полным traceback
logger.exception(f"Функция {func.__name__} завершилась с ошибкой: {e}")
raise # Пробрасываем исключение дальше
return wrapper
# Использование в тестовом классе
class TestUserRegistration:
@log_execution
def test_create_user(self, username, email):
# ... логика теста ...
return user_id
2. Декоратор для повторных попыток (Retry) для нестабильных операций
Крайне полезен в UI-автоматизации (Selenium) или при работе с нестабильными API/сетевыми вызовами, где возможны случайные таймауты или временные ошибки.
import time
import functools
def retry_on_failure(max_attempts=3, delay=1, exceptions=(Exception,)):
"""
Повторяет выполнение функции при возникновении указанных исключений.
"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
last_exception = None
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except exceptions as e:
last_exception = e
if attempt < max_attempts:
time.sleep(delay * attempt) # Прогрессивная задержка
print(f"Попытка {attempt} для {func.__name__} не удалась. Повтор...")
# Если все попытки исчерпаны
raise Exception(f"Функция {func.__name__} не выполнилась после {max_attempts} попыток.") from last_exception
return wrapper
return decorator
# Использование для нестабильного API-запроса
class APITests:
@retry_on_failure(max_attempts=5, delay=2, exceptions=(ConnectionError, TimeoutError))
def get_unstable_api_data(self, url):
response = requests.get(url, timeout=5)
response.raise_for_status()
return response.json()
3. Декоратор для измерения времени выполнения (Performance Check)
Позволяет выявлять деградацию производительности тестов или проверять выполнение операций в рамках SLA.
import time
import functools
def measure_time(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start_time = time.perf_counter()
result = func(*args, **kwargs)
end_time = time.perf_counter()
execution_time = end_time - start_time
print(f"[ТАЙМИНГ] Функция {func.__name__} выполнилась за {execution_time:.4f} секунд")
# Также можно логировать в систему мониторинга (например, InfluxDB + Grafana)
return result
return wrapper
# Использование для критичных по времени операций
class DataBaseTests:
@measure_time
def test_bulk_insert_performance(self, data_set):
# ... массовая вставка данных ...
pass
4. Декоратор для условного пропуска тестов (Conditional Skip) в зависимости от конфигурации
Позволяет динамически управлять набором выполняемых тестов на основе флагов окружения, версии приложения или доступности внешних сервисов.
import os
import functools
def skip_if(condition, reason):
"""
Пропускает тест, если условие condition истинно.
"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
if condition:
print(f"Тест {func.__name__} пропущен. Причина: {reason}")
return None # Или можно использовать raise unittest.SkipTest(reason)
return func(*args, **kwargs)
return wrapper
return decorator
# Использование
IS_PRODUCTION = os.getenv("ENV") == "prod"
class SmokeTests:
@skip_if(condition=IS_PRODUCTION, reason="Деструктивный тест не должен выполняться на Prod")
def test_delete_user(self):
# ... логика теста ...
pass
Ключевые принципы, которые я учитываю при создании декораторов
- Сохранение метаданных: Всегда использую
@functools.wraps(func)для корректной работы интроспекции (имя функции, документация). Это критично для корректного отображения имен тестов в Allure, pytest или unittest отчетах. - Гибкость и параметризация: Часто реализую декораторы через фабрику декораторов (функцию, возвращающую декоратор, как в
retry_on_failure), чтобы можно было передавать параметры (max_attempts,delay). - Обработка исключений: В декораторах для тестов важно аккуратно обрабатывать исключения: логировать, но зачастую пробрасывать их дальше, чтобы test runner корректно интерпретировал результат теста как FAIL, а не как ошибку в декораторе.
- Сочетаемость: Декораторы должны корректно работать в стеке (когда на функцию навешано несколько декораторов). Порядок применения может влиять на поведение.
- Утилитарность и переиспользование: Лучшие декораторы абстрагированы от конкретной бизнес-логики и вынесены в общие модули (
utils/decorators.py), откуда их можно импортировать в любом проекте.
Таким образом, написание декораторов — это не просто академическое упражнение, а практический навык, который напрямую влияет на надежность, поддерживаемость и информативность автоматизированных тестов. Они позволяют создавать чистый, DRY (Don't Repeat Yourself) код, где сама тестовая логика сосредоточена на проверке требований, а вспомогательная функциональность инкапсулирована в декораторы.