Перейти к основному контенту
Tech Path Finder
КурсыИнтервьюКод-ревьюБлог
Tech Path Finder

Персонализированный путеводитель в IT. Квизы, мок-интервью, код ревью и аналитика прогресса.

@potapov_me

Платформа

  • Курсы
  • Прогресс
  • Мок-интервью
  • Код ревью
  • Живое ревью с ИИ
  • Тренажёр переговоров
  • Закладки

Контент

  • Блог
  • Главная
  • Обратная связь

Компания

  • О проекте
  • Тарифы
  • Условия использования
  • Конфиденциальность
  • Согласие на обработку данных
  • Cookie
  • Реквизиты

Аккаунт

  • Войти
  • Зарегистрироваться
  • Профиль

© 2026 Tech Path Finder. Все права защищены.

·ИП Потапов К.С.·Политика конфиденциальности·
Сделано с ❤️ в России
  1. Создание собственных стратегий
custom_strategies

Создание собственных стратегий

Написание кастомных стратегий, @composite, recursive стратегии

Создание собственных стратегий

«Когда встроенных стратегий недостаточно, вы создаёте свои — это сила Hypothesis.»

#Зачем нужны кастомные стратегии?

Встроенные стратегии покрывают базовые типы, но в production вы работаете с доменными объектами:

# Вместо этого: @given(st.integers(), st.text(), st.floats()) def test_user(user_id, name, balance): user = User(user_id, name, balance) # Собираете объект вручную ... # Лучше так: @given(user_strategy()) def test_user(user): ... # Работаете с готовым доменным объектом

Преимущества кастомных стратегий:

  • Инкапсуляция логики генерации
  • Переиспользование между тестами
  • Лучшее shrinking для доменных типов
  • Документирование инвариантов типа

#@composite: основа кастомных стратегий

#Базовый синтаксис

from hypothesis import strategies as st from hypothesis import given @st.composite def positive_integers(draw): """Генерирует положительные целые числа""" return draw(st.integers(min_value=1, max_value=1000000)) @given(positive_integers()) def test_positive(x): assert x > 0

Ключевые моменты:

  • Декоратор @st.composite превращает функцию в стратегию
  • Первый аргумент draw — метод для генерации промежуточных значений
  • Функция должна вернуть сгенерированное значение

#Draw: генерация промежуточных значений

draw() принимает стратегию и возвращает сгенерированное значение:

@st.composite def email_address(draw): """Генерирует валидный email""" domains = draw(st.sampled_from(['gmail.com', 'yahoo.com', 'example.org'])) username = draw(st.text( alphabet=st.characters(whitelist_categories=('Lu', 'Ll', 'Nd', '_')), min_size=1, max_size=20 )) return f"{username}@{domains}"

#Пример: стратегия для даты рождения

from datetime import date @st.composite def birth_date(draw): """Генерирует валидную дату рождения для человека 18-100 лет""" today = date.today() max_year = today.year - 18 min_year = today.year - 100 year = draw(st.integers(min_value=min_year, max_value=max_year)) month = draw(st.integers(min_value=1, max_value=12)) # Корректный день для месяца import calendar max_day = calendar.monthrange(year, month)[1] day = draw(st.integers(min_value=1, max_value=max_day)) return date(year, month, day)

Преимущество перед builds(): логика для вычисления максимального дня в месяце.

#Сложные кастомные стратегии

#Стратегия для денежной суммы

from decimal import Decimal @st.composite def monetary_amount(draw, min_value=Decimal('0.01'), max_value=Decimal('999999.99')): """ Генерирует денежную сумму с двумя знаками после точки. Args: min_value: Минимальная сумма max_value: Максимальная сумма """ # Генерируем количество центов для точного представления min_cents = int(min_value * 100) max_cents = int(max_value * 100) cents = draw(st.integers(min_value=min_cents, max_value=max_cents)) return Decimal(cents) / 100

Почему не floats? Floats неточны для денег. Decimal с фиксированными знаками — правильный выбор.

#Стратегия для телефонного номера

import re @st.composite def phone_number(draw, country_code='+1'): """Генерирует валидный телефонный номер""" # Генерируем цифры digits = draw(st.lists( st.integers(min_value=0, max_value=9), min_size=10, max_size=10 )) # Форматируем: (XXX) XXX-XXXX area = ''.join(map(str, digits[:3])) prefix = ''.join(map(str, digits[3:6])) line = ''.join(map(str, digits[6:])) return f"{country_code} ({area}) {prefix}-{line}"

#Стратегия для UUID с версией

import uuid @st.composite def uuid_with_version(draw, version=4): """Генерирует UUID определённой версии""" if version == 4: # UUID v4 — случайный return draw(st.uuids()) elif version == 1: # UUID v1 — на основе времени return uuid.uuid1() else: # Для других версий генерируем случайный и модифицируем base = draw(st.uuids()) # Модификация для нужной версии (упрощённо) return base

#Рекурсивные стратегии для деревьев

#Стратегия для бинарного дерева

from typing import Optional, Union from dataclasses import dataclass @dataclass class TreeNode: value: int left: Optional['TreeNode'] = None right: Optional['TreeNode'] = None @st.composite def binary_tree(draw, min_value=0, max_value=100, max_depth=5): """ Генерирует бинарное дерево. Args: min_value: Минимальное значение узла max_value: Максимальное значение узла max_depth: Максимальная глубина дерева """ # Решаем, будет ли узел листом (None) if max_depth == 0 or draw(st.floats()) < 0.3: return None value = draw(st.integers(min_value=min_value, max_value=max_value)) left = draw(binary_tree(min_value, max_value, max_depth - 1)) right = draw(binary_tree(min_value, max_value, max_depth - 1)) return TreeNode(value=value, left=left, right=right) # Использование @given(binary_tree()) def test_tree_size(tree): def count_nodes(node): if node is None: return 0 return 1 + count_nodes(node.left) + count_nodes(node.right) if tree: assert count_nodes(tree) >= 1

#Стратегия для JSON

@st.composite def json_value(draw, max_depth=5): """Генерирует валидное JSON-значение""" if max_depth == 0: # Базовые значения на глубине 0 return draw(st.one_of( st.none(), st.booleans(), st.integers(), st.floats(allow_nan=False, allow_infinity=False), st.text() )) # Рекурсивно: массив или объект if draw(st.booleans()): # Массив return draw(st.lists(json_value(max_depth=max_depth - 1), max_size=5)) else: # Объект return draw(st.dictionaries( st.text(min_size=1, max_size=10), json_value(max_depth=max_depth - 1), max_size=5 ))

#Параметризованные стратегии

#Стратегия с настройками

@st.composite def user_with_role(draw, role=None): """ Генерирует пользователя с опциональной ролью. Args: role: Если указана, пользователь будет с этой ролью """ user_id = draw(st.integers(min_value=1, max_value=1000000)) email = draw(email_address()) if role is None: role = draw(st.sampled_from(['user', 'admin', 'moderator'])) return { 'id': user_id, 'email': email, 'role': role, 'created_at': draw(st.datetimes()) } # Использование: @given(user_with_role()) # Случайная роль def test_any_user(user): ... @given(user_with_role(role='admin')) # Только админы def test_admin_user(user): assert user['role'] == 'admin'

#Стратегия для paginated response

from typing import List, Generic, TypeVar T = TypeVar('T') @st.composite def paginated_response(draw, item_strategy, page_size=10): """ Генерирует пагинированный ответ API. Args: item_strategy: Стратегия для элементов page_size: Размер страницы """ page = draw(st.integers(min_value=1, max_value=100)) total_items = draw(st.integers(min_value=0, max_value=10000)) items = draw(st.lists(item_strategy, max_size=page_size)) return { 'page': page, 'page_size': page_size, 'total_items': total_items, 'items': items, 'has_next': page * page_size < total_items, 'has_prev': page > 1 } # Использование @given(paginated_response(st.integers())) def test_pagination(response): if response['page'] == 1: assert not response['has_prev']

#Регистрация стратегий для типов

Hypothesis позволяет регистрировать стратегии для пользовательских типов:

from hypothesis import register_type_strategy from dataclasses import dataclass @dataclass class Money: amount: Decimal currency: str def money_strategy(): return st.builds( Money, amount=st.decimals(min_value=0, max_value=1000000, places=2), currency=st.sampled_from(['USD', 'EUR', 'RUB']) ) # Регистрация register_type_strategy(Money, money_strategy()) # Теперь Hypothesis автоматически генерирует Money для аннотаций @given(st.builds(lambda m: m, st.from_type(Money))) def test_money(m: Money): assert m.amount >= 0

#Production-пример: стратегия для заказа

from enum import Enum from datetime import datetime, timedelta from decimal import Decimal from typing import List from dataclasses import dataclass, field class OrderStatus(Enum): PENDING = 'pending' PAID = 'paid' SHIPPED = 'shipped' DELIVERED = 'delivered' CANCELLED = 'cancelled' @dataclass class OrderItem: product_id: int quantity: int price: Decimal @dataclass class Order: id: int customer_id: int items: List[OrderItem] status: OrderStatus created_at: datetime updated_at: datetime discount: Decimal = field(default_factory=lambda: Decimal('0')) @st.composite def order_item(draw): return OrderItem( product_id=draw(st.integers(min_value=1, max_value=10000)), quantity=draw(st.integers(min_value=1, max_value=100)), price=draw(st.decimals(min_value=Decimal('0.01'), max_value=Decimal('10000'), places=2)) ) @st.composite def order(draw): created_at = draw(st.datetimes( min_value=datetime(2020, 1, 1), max_value=datetime.now() )) # updated_at >= created_at delta = draw(st.timedeltas(min_value=timedelta(0), max_value=timedelta(days=365))) updated_at = created_at + delta items = draw(st.lists(order_item(), min_size=1, max_size=10)) # Статус с весами (реалистичное распределение) status = draw(st.sampled_from([ OrderStatus.PENDING, OrderStatus.PENDING, # Больше pending OrderStatus.PAID, OrderStatus.PAID, # Больше paid OrderStatus.SHIPPED, OrderStatus.DELIVERED, OrderStatus.CANCELLED, ])) discount = draw(st.decimals(min_value=Decimal('0'), max_value=Decimal('0.5'), places=2)) return Order( id=draw(st.integers(min_value=1, max_value=1000000)), customer_id=draw(st.integers(min_value=1, max_value=100000)), items=items, status=status, created_at=created_at, updated_at=updated_at, discount=discount ) # Тесты @given(order()) def test_order_updated_after_created(order): assert order.updated_at >= order.created_at @given(order()) def test_order_discount_in_range(order): assert Decimal('0') <= order.discount <= Decimal('0.5') @given(order()) def test_order_items_positive(order): for item in order.items: assert item.quantity > 0 assert item.price > 0

#Заключение

Кастомные стратегии — это мощный инструмент для:

  1. Инкапсуляции логики генерации доменных объектов
  2. Переиспользования между тестами
  3. Улучшения shrinking для специфичных типов
  4. Документирования инвариантов типа

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

  • @st.composite + draw() — основа кастомных стратегий
  • Параметризуйте стратегии для гибкости
  • Используйте рекурсию для деревьев и вложенных структур
  • Регистрируйте стратегии для автоматического использования

Следующая тема: Stateful Testing — RuleBasedStateMachine, инварианты, тестирование сложных систем с состоянием.

Далее: Stateful Testing и машины состояний