Все встроенные стратегии, композиция, фильтрация, mapped strategies
«Стратегии — это язык, на котором вы описываете пространство входных данных для ваших тестов.»
Стратегия — это объект Hypothesis, который описывает, как генерировать значения определённого типа. Стратегии являются ленивыми (не генерируют данные до запроса) и композируемыми (можно комбинировать для создания сложных структур).
from hypothesis import strategies as st
# Стратегия не генерирует данные до вызова example()
int_strategy = st.integers()
value = int_strategy.example() # Например: 42st.integers()
# Генерирует: 0, 1, -1, 2147483647, -2147483648, 100, ...
st.integers(min_value=0, max_value=100)
# Генерирует: 0, 1, 50, 99, 100
st.integers(min_value=-10, max_value=-1)
# Генерирует: -10, -5, -1Особенности:
st.floats()
# Генерирует: 0.0, -0.0, inf, -inf, nan, 1e-308, 1e308, ...
st.floats(allow_nan=False, allow_infinity=False)
# Исключает специальные значения
st.floats(min_value=0.0, max_value=1.0)
# Генерирует: 0.0, 0.5, 1.0, 0.001, 0.999
st.floats(width=32) # 32-битные float
st.floats(width=64) # 64-битные double (по умолчанию)Важно: allow_nan=False может замедлить генерацию, так как Hypothesis будет отбрасывать NaN значения.
st.decimals()
# Генерирует: Decimal('0'), Decimal('1.5'), Decimal('NaN'), ...
st.decimals(allow_nan=False, allow_infinity=False)
st.decimals(min_value=Decimal('-100'), max_value=Decimal('100'))
st.decimals(places=2) # Фиксированное количество знаков после точкиst.text()
# Генерирует: "", "0", "A", "\x00", "\U0001F600", "A" * 100, ...
st.text(min_size=1, max_size=10)
# Непустые строки длиной 1-10 символов
st.text(alphabet=st.characters(whitelist_categories=('Lu', 'Ll')))
# Только буквы (uppercase и lowercase)
st.binary()
# Генерирует: b"", b"\x00", b"\xff", b"abc", ...
st.binary(min_size=1, max_size=100)
st.characters()
# Генерирует: специальные символы, ASCII, Unicode
st.characters(whitelist_categories=('Lu', 'Ll', 'Nd'))
# Только буквы и цифрыСпециальные символы, которые автоматически генерирует st.text():
"""\x00""\n", "\r\n""\t""\U0001F600" (эмодзи), "\u0000" (null)"\x00\n", "A\x00B"st.booleans()
# Генерирует: True, False
st.none()
# Генерирует: None
st.just(42)
# Всегда генерирует: 42st.lists(st.integers())
# Генерирует: [], [0], [1, 2, 3], [0, 0, 0], ...
st.lists(st.integers(), min_size=1, max_size=10)
# Непустые списки длиной 1-10
st.lists(st.integers(), unique=True)
# Уникальные элементы
st.lists(st.integers(), unique_by=lambda x: x % 2)
# Уникальность по ключу (чётность)st.sets(st.integers())
# Генерирует: set(), {0}, {1, 2, 3}, ...
st.frozensets(st.text())
# Генерирует: frozenset(), {""}, {"a", "b"}, ...st.dictionaries(st.text(), st.integers())
# Генерирует: {}, {"": 0}, {"a": 1, "b": 2}, ...
st.dictionaries(
st.text(min_size=1, max_size=5),
st.integers(min_value=0, max_value=100),
min_size=1,
max_size=10
)st.tuples(st.integers(), st.text(), st.booleans())
# Генерирует: (0, "", True), (42, "abc", False), ...
st.tuples(st.integers(), st.integers())
# Фиксированная длина: (0, 0), (1, -1), ...st.one_of(st.integers(), st.text())
# Генерирует: 0, "", 42, "abc", ...
st.one_of(st.integers(), st.text(), st.lists(st.integers()))
# Три типа данных
st.integers() | st.text() # Альтернативный синтаксис
# Эквивалентно st.one_of(st.integers(), st.text())
st.optional(st.integers(), default=None)
# Генерирует: None, 0, 42, ...st.integers().map(str)
# Генерирует: "0", "42", "-1", ...
st.integers().map(lambda x: x * 2)
# Генерирует: 0, 2, -2, 84, ...
st.text().map(len)
# Генерирует: 0, 1, 5, 100, ...Важно: map применяется к каждому сгенерированному значению. Shrinking работает на уровне исходной стратегии.
st.integers().filter(lambda x: x > 0)
# Генерирует только положительные: 1, 2, 50, 100, ...
st.text().filter(lambda s: len(s) >= 3)
# Строки длиной минимум 3 символа
st.integers().filter(lambda x: x % 2 == 0)
# Только чётные числаПредупреждение: Если фильтр слишком строгий, Hypothesis не сможет генерировать достаточно примеров. Используйте assume() внутри теста для сложной фильтрации.
# flatmap позволяет использовать сгенерированное значение для создания новой стратегии
st.integers(min_value=1, max_value=10).flatmap(
lambda n: st.lists(st.integers(), min_size=n, max_size=n)
)
# Генерирует списки, где длина равна первому сгенерированному числу# Деревья: лист — целое число, узел — список детей
st.recursive(
st.integers(), # Базовый случай (листья)
lambda children: st.lists(children, max_size=3), # Рекурсивный случай
max_leaves=10
)
# Генерирует: 0, [0, 1], [[0], 1, 2], ...st.functions()
# Генерирует mock-функции
st.functions(like=lambda x: x, returns=st.integers())
# Функция с определённой сигнатуройemail_strategy = (
st.builds(
"{}@{}.{}".format,
st.text(alphabet=st.characters(whitelist_categories=('Lu', 'Ll', 'Nd')), min_size=1, max_size=10),
st.text(alphabet=st.characters(whitelist_categories=('Lu', 'Ll', 'Nd')), min_size=2, max_size=10),
st.sampled_from(['com', 'org', 'net', 'ru', 'io'])
)
)from datetime import date
birth_date_strategy = st.builds(
date,
year=st.integers(min_value=1950, max_value=2010),
month=st.integers(min_value=1, max_value=12),
day=st.integers(min_value=1, max_value=31)
)Проблема: эта стратегия может генерировать невалидные даты (31 февраля). Решение:
from datetime import date
import calendar
def is_valid_date(year, month, day):
try:
date(year, month, day)
return True
except ValueError:
return False
birth_date_strategy = st.builds(
date,
year=st.integers(min_value=1950, max_value=2010),
month=st.integers(min_value=1, max_value=12),
day=st.integers(min_value=1, max_value=31)
).filter(lambda d: is_valid_date(d.year, d.month, d.day))Или лучше через @composite (см. следующую тему).
json_strategy = st.recursive(
st.one_of(
st.none(),
st.booleans(),
st.integers(),
st.floats(allow_nan=False, allow_infinity=False),
st.text()
),
lambda children: st.one_of(
st.lists(children, max_size=5),
st.dictionaries(st.text(), children, max_size=5)
),
max_leaves=20
)# Фильтрация при генерации
positive_integers = st.integers().filter(lambda x: x > 0)
@given(positive_integers)
def test_positive(x):
assert x > 0Плюсы:
Минусы:
@given(st.integers(), st.integers())
def test_division(x, y):
assume(y != 0) # Исключаем деление на ноль
assume(x > 0) # Только положительные x
result = x / y
assert isinstance(result, float)Плюсы:
Минусы:
| Ситуация | Решение |
|---|---|
| Простой фильтр (x > 0) | filter() |
| Сложный фильтр с несколькими условиями | assume() |
| Фильтр используется в нескольких тестах | filter() |
| Фильтр зависит от других параметров | assume() |
| Риск слишком строгого фильтра | assume() с предупреждением |
Важное свойство map: shrinking работает на уровне исходной стратегии, а не преобразованной.
@given(st.integers().map(lambda x: x * 100))
def test_mapped(x):
# x всегда кратно 100
assert x % 100 == 0Если тест падает на x = 10000, shrinker попытается уменьшить исходное число:
Вы увидите failing case: x = 100 (минимальное кратное 100).
from decimal import Decimal
from datetime import datetime, timedelta
def order_strategy():
return st.builds(
dict,
order_id=st.uuids(),
customer_id=st.integers(min_value=1, max_value=1000000),
items=st.lists(
st.builds(
dict,
product_id=st.integers(min_value=1, max_value=10000),
quantity=st.integers(min_value=1, max_value=100),
price=st.decimals(min_value=0.01, max_value=10000, places=2)
),
min_size=1,
max_size=10
),
created_at=st.datetimes(
min_value=datetime(2020, 1, 1),
max_value=datetime.now()
),
discount=st.decimals(min_value=0, max_value=0.5, places=2)
)
@given(order_strategy())
def test_order_total_positive(order):
total = sum(item['quantity'] * item['price'] for item in order['items'])
total_with_discount = total * (1 - order['discount'])
assert total_with_discount >= 0from urllib.parse import urljoin
def api_url_strategy(base_path: str = "/api/v1"):
paths = st.sampled_from([
"/users", "/products", "/orders",
"/cart", "/checkout", "/search"
])
ids = st.integers(min_value=1, max_value=1000000).map(str)
query_params = st.dictionaries(
st.sampled_from(["page", "limit", "sort", "filter", "q"]),
st.one_of(st.integers(), st.text()),
max_size=3
)
return st.builds(
lambda path, id_, params: f"{base_path}{path}/{id_}?{params}",
paths,
ids,
query_params.map(lambda p: "&".join(f"{k}={v}" for k, v in p.items()))
)Стратегии — это фундамент Hypothesis. Понимание всех встроенных стратегий и умение их комбинировать позволяет описывать сложные пространства входных данных.
Ключевые выводы:
builds, one_of, tuplesfilter() для простых условий, assume() для сложныхmap применяется после генерации, shrinking работает на исходной стратегииrecursive для деревьев и вложенных структурВ следующей теме вы научитесь создавать собственные стратегии с помощью @composite.
Следующая тема: Создание собственных стратегий — @composite, draw(), рекурсивные стратегии для доменных типов.
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.