Прямой ответ
Property-based тестирование (PBT) проверяет не конкретные пары «вход — ожидаемый выход», а свойства, которые должны выполняться для всех допустимых входов. Вместо того чтобы писать «для входа 7 результат равен 12», разработчик формулирует инвариант: «функция идемпотентна», «сортировка сохраняет длину списка», «результат всегда валиден по схеме».
Библиотека Hypothesis в Python генерирует сотни и тысячи входных значений по заданным правилам, подставляет их в тест и ищет контрпример. Если находит — сжимает его до минимального воспроизводимого случая и сохраняет в базе примеров, чтобы в следующих прогонах начинать именно с него.
Ключевые понятия:
- Стратегия — объект, описывающий, как генерировать значения:
st.integers(),st.text(),st.lists(st.floats()),st.builds(...). - Инвариант (свойство) — утверждение, истинное для всех сгенерированных значений.
- Shrinking — автоматическое уменьшение найденного контрпримера: список из 47 элементов превращается в список из одного-двух, сохраняя ошибку.
- База примеров — каталог
.hypothesis, где хранятся найденные падения; его стоит добавлять в.gitignore, но не удалять между прогонами.
from hypothesis import given, strategies as st, settings, example
@given(st.lists(st.integers()))
@example([]) # явный граничный случай
def test_sort_preserves_multiset(xs):
result = sorted(xs)
assert len(result) == len(xs)
assert sorted(result) == sorted(xs) # мультимножество сохранено
assert all(a <= b for a, b in zip(result, result[1:])) # упорядоченность
Типичные свойства, которые легко формулировать:
- Обратимость.
decode(encode(x)) == x. - Идемпотентность.
f(f(x)) == f(x). - Инвариант операции. Сумма элементов не меняется при перестановке.
- Соответствие эталону. Своя реализация совпадает с заведомо правильной на всех входах.
- Валидность результата. Результат проходит проверку схемы независимо от входа.
- Монотонность. Увеличение входа не уменьшает выход.
Практика: как применять без разочарований
1. Начинайте с обратимости и эталонов. Это самые дешёвые свойства: не нужно придумывать сложный инвариант, достаточно пары функций.
2. Сравнивайте с эталонной реализацией. Если есть медленная, но очевидная реализация, проверяйте быструю против неё. Такой тест находит расхождения на входах, которые человек не предусмотрел бы.
@given(st.lists(st.integers(min_value=-10**6, max_value=10**6), max_size=200))
def test_fast_median_matches_reference(xs):
assert fast_median(xs) == reference_median(xs)
3. Управляйте бюджетом. По умолчанию Hypothesis выполняет 100 примеров на тест. Для медленных тестов это много; для критичных — мало.
@settings(max_examples=500, deadline=None) # deadline=None для тестов с вводом-выводом
@given(st.text())
def test_slug_is_url_safe(s):
slug = make_slug(s)
assert re.fullmatch(r"[a-z0-9\-]+", slug)
4. Состояние и последовательности операций. Для протоколов и конечных автоматов Hypothesis предлагает stateful-тестирование: генерируется не одно значение, а последовательность команд, и после каждой проверяются инварианты. Это самый мощный, но и самый сложный режим.
5. Не заменяйте обычные тесты. Примерные тесты читаются как документация: «вот вход, вот ожидаемый результат». Свойства дополняют их, покрывая пространство входов. Практика — оба подхода в одном файле, см. фикстуры и параметризацию pytest.
Рекомендации:
- Формулируйте свойство до написания кода. Если свойство сформулировать трудно, возможно, интерфейс функции неудобен.
- Не проверяйте реализацию саму собой. Свойство
f(x) == f(x)ничего не даёт; нужна независимая проверка. - Следите за временем прогона. Тысячи примеров в unit-тестах замедляют обратную связь. Основной бюджет стоит переносить в ночной прогон.
- Используйте сжатие как инструмент отладки. Минимальный контрпример часто сразу показывает причину: пустая строка, отрицательное число, символ вне BMP.
- Отличайте PBT от фаззинга. PBT проверяет сформулированные свойства на структурированных входах; фаззинг ищет краши и нарушения безопасности на произвольных байтах — см. фаззинг в Python.
Практический сценарий: PBT нашёл ошибку в парсере диапазонов дат
Контекст: функция разбирала строку вида 2026-01-01..2026-03-15 в пару дат. Обычные тесты на десяти примерах проходили.
- Свойство. Сформулировали три: начало диапазона не позже конца; повторная сериализация даёт ту же строку; результат всегда валиден по схеме.
- Запуск. Hypothesis нашёл падение на 63-м примере и сжал его до
2026-02-29..2026-03-01. - Причина. Валидация использовала фиксированный список дней в месяце без учёта високосного года, поэтому 29 февраля невисокосного года принималось, а дальше падало при построении даты.
- Дополнительно. Второй прогон с большим бюджетом нашёл падение на строке с пробелом вокруг разделителя: парсер допускал его на входе, но сериализатор — нет, из-за чего нарушалась обратимость.
- Исправление. Перешли на стандартный конструктор дат, нормализацию пробелов добавили в парсер; найденные контрпримеры зафиксировали как обычные тесты с явными входами.
Вывод: два найденных дефекта оказались на границах, которые человек не проверяет вручную: високосный год и невидимые пробелы. Стоимость — одно сформулированное свойство вместо десятка примеров.
FAQ
Чем PBT отличается от параметризованного теста? Параметризованный тест перебирает фиксированный список входов, который придумал человек. PBT генерирует входы по правилам и ищет те, на которых свойство нарушается, а затем уменьшает найденный случай до минимума.
Что делать, если тест нестабилен между прогонами? Hypothesis детерминирована при фиксированном seed и повторяет найденные ранее падения из базы примеров. Нестабильность обычно означает, что проверяемое свойство действительно не выполняется для части входов, либо тест зависит от внешнего состояния.
Подходит ли PBT для кода с вводом-выводом?
Да, но с оговорками: нужно отключить ограничение времени (deadline=None) и генерировать не запросы к внешней системе, а данные, которые в неё пойдут. Саму внешнюю систему подменяют двойником — см. тестовые двойники.
Как понять, что свойств достаточно? Прямого критерия нет. Косвенный — мутационное тестирование: если внесённые в код мутации не обнаруживаются тестами, свойств не хватает. Методика — в статье про мутационное тестирование.
Источники: Hypothesis docs, pytest docs, Wikipedia (QuickCheck) — ссылки в шапке.